├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release-gem.yml │ ├── tag.yml │ ├── test-draw.yml │ ├── test.yml │ └── utils.rb ├── .gitignore ├── .yardopts ├── CONTRIBUTING.md ├── ChangeLog.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── examples ├── breakout.rb ├── camera.rb ├── clock.rb ├── delay_camera.rb ├── filter.rb ├── hello.rb ├── image.rb ├── shake.rb └── shapes.rb ├── lib ├── processing.rb └── processing │ ├── all.rb │ ├── app.rb │ ├── capture.rb │ ├── context.rb │ ├── events.rb │ ├── extension.rb │ ├── font.rb │ ├── graphics.rb │ ├── graphics_context.rb │ ├── image.rb │ ├── shader.rb │ ├── shape.rb │ ├── svg.rb │ ├── touch.rb │ ├── vector.rb │ └── window.rb ├── processing.gemspec └── test ├── browser.rb ├── helper.rb ├── test_capture.rb ├── test_color.rb ├── test_font.rb ├── test_graphics.rb ├── test_graphics_context.rb ├── test_image.rb ├── test_shader.rb ├── test_shape.rb ├── test_svg.rb ├── test_text_bounds.rb ├── test_touch.rb ├── test_utility.rb └── test_vector.rb /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pull Requests Not Accepted 🚫 2 | 3 | Thank you for your interest in contributing! 4 | However, this repository does not accept pull requests directly. 5 | 6 | ### Where to Contribute? 7 | 8 | Please submit your changes to the [xord/all](https://github.com/xord/all) monorepo, which serves as the primary repository for all our main libraries. 9 | 10 | For more details, please refer to our [contribution guidelines](../CONTRIBUTING.md). 11 | 12 | Thanks for your understanding! 🙌 13 | -------------------------------------------------------------------------------- /.github/workflows/release-gem.yml: -------------------------------------------------------------------------------- 1 | name: Release Gem 2 | 3 | on: 4 | push: 5 | tags: ['v[0-9]*'] 6 | 7 | jobs: 8 | release: 9 | runs-on: macos-latest 10 | 11 | steps: 12 | - name: ruby 3.2 13 | uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: 3.2 16 | 17 | - name: checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: setup gems 21 | run: bundle install 22 | 23 | - name: setup dependencies 24 | run: "ruby -I.github/workflows -rutils -e 'setup_dependencies'" 25 | 26 | - name: test 27 | run: bundle exec rake quiet packages test 28 | 29 | - name: create gem 30 | id: gem 31 | run: | 32 | bundle exec rake gem 33 | echo path=$(ruby -e 'print Dir.glob("*.gem").first') >> $GITHUB_OUTPUT 34 | 35 | - name: create github release 36 | id: release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | tag_name: ${{ github.ref }} 42 | release_name: ${{ github.ref }} 43 | 44 | - name: upload to github release 45 | uses: actions/upload-release-asset@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.release.outputs.upload_url }} 50 | asset_path: ./${{ steps.gem.outputs.path }} 51 | asset_name: ${{ steps.gem.outputs.path }} 52 | asset_content_type: application/zip 53 | 54 | - name: upload to rubygems 55 | env: 56 | GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_AUTH_TOKEN }} 57 | run: | 58 | mkdir -p $HOME/.gem 59 | touch $HOME/.gem/credentials 60 | chmod 0600 $HOME/.gem/credentials 61 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 62 | bundle exec rake upload 63 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | tag: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: ruby 3.2 13 | uses: ruby/setup-ruby@v1 14 | with: 15 | ruby-version: 3.2 16 | 17 | - name: checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | token: ${{ secrets.PAT }} 22 | 23 | - name: setup dependencies 24 | run: "ruby -I.github/workflows -rutils -e 'setup_dependencies only: :xot'" 25 | 26 | - name: setup user name and email 27 | run: | 28 | git config --global user.email "xordog@gmail.com" 29 | git config --global user.name "xord" 30 | 31 | - name: tag versions 32 | run: "ruby -I.github/workflows -rutils -e 'tag_versions'" 33 | 34 | - name: push tags 35 | run: git push origin --tags 36 | -------------------------------------------------------------------------------- /.github/workflows/test-draw.yml: -------------------------------------------------------------------------------- 1 | name: Test Draw 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: ruby 3.2 14 | uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: 3.2 17 | 18 | - name: checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: setup gems 22 | run: bundle install 23 | 24 | - name: setup dependencies 25 | run: "ruby -I.github/workflows -rutils -e 'setup_dependencies'" 26 | 27 | - name: setup chrome 28 | uses: browser-actions/setup-chrome@latest 29 | with: 30 | chrome-version: stable 31 | 32 | - name: test 33 | run: rake test:draw 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - name: ruby 3.2 14 | uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: 3.2 17 | 18 | - name: checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: setup gems 22 | run: bundle install 23 | 24 | - name: setup dependencies 25 | run: "ruby -I.github/workflows -rutils -e 'setup_dependencies'" 26 | 27 | - name: packages 28 | run: bundle exec rake packages 29 | 30 | - name: lib 31 | run: bundle exec rake lib 32 | 33 | - name: ext 34 | run: bundle exec rake ext 35 | 36 | - name: test 37 | run: bundle exec rake test 38 | -------------------------------------------------------------------------------- /.github/workflows/utils.rb: -------------------------------------------------------------------------------- 1 | RENAMES = {reflex: 'reflexion'} 2 | 3 | def sh(cmd) 4 | puts cmd 5 | system cmd 6 | end 7 | 8 | def setup_dependencies(build: true, only: nil) 9 | gemspec_path = `git ls-files`.lines(chomp: true).find {|l| l =~ /\.gemspec$/} 10 | return unless gemspec_path 11 | 12 | gemspec = File.read gemspec_path 13 | name = File.basename gemspec_path, '.gemspec' 14 | 15 | exts = File.readlines('Rakefile') 16 | .map {|l| l[%r|^\s*require\W+(\w+)/extension\W+$|, 1]} 17 | .compact 18 | .reject {|ext| ext == name} 19 | exts = exts & [only].flatten.map(&:to_s) if only 20 | 21 | exts.each do |ext| 22 | gem = RENAMES[ext.to_sym].then {|s| s || ext} 23 | ver = gemspec[/add_dependency.*['"]#{gem}['"].*['"]\s*>=\s*([\d\.]+)\s*['"]/, 1] 24 | opts = '-c advice.detachedHead=false --depth 1' 25 | clone = "git clone #{opts} https://github.com/xord/#{ext}.git ../#{ext}" 26 | 27 | # 'rake subtree:push' pushes all subrepos, so cloning by new tag 28 | # often fails before tagging each new tag 29 | sh %( #{clone} --branch v#{ver} || #{clone} ) 30 | sh %( cd ../#{ext} && rake ext ) 31 | end 32 | end 33 | 34 | def tag_versions() 35 | tags = `git tag`.lines chomp: true 36 | vers = `git log --oneline ./VERSION` 37 | .lines(chomp: true) 38 | .map {|line| line.split.first[/^\w+$/]} 39 | .map {|hash| [`git cat-file -p #{hash}:./VERSION 2>/dev/null`[/[\d\.]+/], hash]} 40 | .select {|ver, hash| ver && hash} 41 | .reverse 42 | .to_h 43 | 44 | changes = File.read('ChangeLog.md') 45 | .split(/^\s*##\s*\[\s*v([\d\.]+)\s*\].*$/) 46 | .slice(1..-1) 47 | .each_slice(2) 48 | .to_h 49 | .transform_values(&:strip!) 50 | 51 | vers.to_a.reverse.each do |ver, hash| 52 | tag = "v#{ver}" 53 | break if tags.include?(tag) 54 | sh %( git tag -a -m \"#{changes[ver]&.gsub '"', '\\"'}\" #{tag} #{hash} ) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *~ 3 | 4 | .yardoc 5 | .DS_Store 6 | doc 7 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | Thank you for your interest in contributing! 4 | However, this repository does not accept pull requests. 5 | Instead, please submit your changes to the [xord/all](https://github.com/xord/all) monorepo, which serves as the primary repository for all our main libraries. 6 | 7 | For any questions, feel free to open an issue. 8 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # processing ChangeLog 2 | 3 | 4 | ## [v1.1.12] - 2025-05-22 5 | 6 | - Add control_change event handler block 7 | 8 | 9 | ## [v1.1.11] - 2025-05-13 10 | 11 | - Update dependencies 12 | 13 | 14 | ## [v1.1.10] - 2025-05-12 15 | 16 | - Update dependencies 17 | 18 | 19 | ## [v1.1.9] - 2025-05-11 20 | 21 | - Update dependencies 22 | 23 | 24 | ## [v1.1.8] - 2025-04-08 25 | 26 | - Update dependencies: xot, rucy, rays, reflex 27 | 28 | 29 | ## [v1.1.7] - 2025-03-24 30 | 31 | - Add PULL_REQUEST_TEMPLATE.md 32 | - Add CONTRIBUTING.md 33 | 34 | 35 | ## [v1.1.6] - 2025-03-07 36 | 37 | - Add keyIsRepeat 38 | - Add Vector#-@ 39 | 40 | - Painter#background: Clearing background with transparency uses blend_mode with :replace to replace alpha value 41 | - Fix p5.rb version 42 | 43 | - Fix smaller-than-expected height of screenshots rendered in headless chrome 44 | 45 | 46 | ## [v1.1.5] - 2025-01-30 47 | 48 | - Do not define snake_case methods by default 49 | 50 | 51 | ## [v1.1.4] - 2025-01-27 52 | 53 | - Event blocks can be defined as methods with 'def' 54 | - Alias snake case methods 55 | - ellipse() can take 3 params 56 | 57 | 58 | ## [v1.1.3] - 2025-01-23 59 | 60 | - Update dependencies 61 | 62 | 63 | ## [v1.1.2] - 2025-01-14 64 | 65 | - Update workflow files 66 | - Set minumum version for runtime dependency 67 | 68 | 69 | ## [v1.1.1] - 2025-01-13 70 | 71 | - Update URLs as p5js.org has been relaunched 72 | - Update LICENSE 73 | 74 | - Fix a bug in which the display magnification was fixed at 1x when the size specified by size() was different from the window size and the aspect ratio of both windows was the same 75 | 76 | 77 | ## [v1.1] - 2024-07-06 78 | 79 | - Support Windows 80 | 81 | 82 | ## [v1.0.3] - 2024-07-05 83 | 84 | - Update workflows for test 85 | - Update to actions/checkout@v4 86 | 87 | 88 | ## [v1.0.2] - 2024-03-14 89 | 90 | - Add 'rexml' to gemspec dependency 91 | 92 | 93 | ## [v1.0.1] - 2024-03-14 94 | 95 | - Add 'rexml' to Gemfile 96 | 97 | 98 | ## [v1.0] - 2024-03-14 99 | 100 | - Add stroke(), setStroke(), and setStrokeWeight() to Shape class 101 | - Add setStrokeCap() and setStrokeJoin() to Shape class 102 | - Add join type 'miter-clip' and 'arcs' 103 | - Add color codes 104 | - Add loadShape() 105 | 106 | - Rename the join type 'SQUARE' to 'BEVEL' 107 | 108 | - Fix that and had half diameters 109 | 110 | 111 | ## [v0.5.34] - 2024-02-16 112 | 113 | - Add '@see' links to documents 114 | - Fix missing nil returning 115 | 116 | 117 | ## [v0.5.33] - 2024-02-07 118 | 119 | - Add curveDetail() and bezierDetail() 120 | - Add curvePoint(), curveTangent(), curveTightness(), bezierPoint(), and bezierTangent() 121 | - Add textLeading() 122 | - Add deltaTime 123 | - Add hue(), saturation(), and brightness() 124 | - Add noiseSeed() and noiseDetail() 125 | - Add randomGaussian() 126 | - Add randomSeed() 127 | - Add rotateX(), rotateY(), and rotateZ() 128 | - Add rotateX(), rotateY(), and rotateZ() to Shape class 129 | - Add shearX() and shearY() 130 | - Add applyMatrix() 131 | - Add printMatrix() 132 | - Add fullscreen() (fullScreen()) function 133 | - Add smooth() and noSmooth() 134 | - Add keyIsDown() 135 | - Add keyIsPressed() 136 | - Add mouseWheel() 137 | - Add doubleClicked() 138 | - Add renderMode() 139 | 140 | - Setup view projection matrix by using perspective() instead of ortho() 141 | - Display window in the center of the screen by default 142 | - loadImage() uses Net::HTTP.get() instead of URI.open() to retrieve images via http(s) 143 | - loadImage() writes a file in streaming mode 144 | - loadImage() raises Net::HTTPClientException instead of OpenURI::HTTPError 145 | - Reimplement the noise() for better compatibility 146 | - push/popStyle() manage colorMode, angleMode, blendMode, and miter_limit states 147 | - size() and createCanvas() resize the window by themselves 148 | - texture_mode/wrap -> texcoord_mode/wrap 149 | - updatePixels() can take block 150 | 151 | - Fix that textFont() did not return current font 152 | - Fix that updatePixels() did not update the texture 153 | - Fix the performance of requestImage() by calling Thread.pass 154 | - Fix an issue with unintended canvas resizing when the screen pixel density changes 155 | - Fix some missing attribute copies on the canvas 156 | - Fix Matrix::to_a order 157 | 158 | 159 | ## [v0.5.32] - 2024-01-08 160 | 161 | - Add requestImage() 162 | - Add texture(), textureMode(), and textureWrap() 163 | - Add loadPixels(), updatePixels(), and pixels() 164 | - Add curveVertex(), bezierVertex(), and quadraticVertex() 165 | - Add beginContour() and endContour() 166 | - Add createFont(), loadFont(), and Font.list() 167 | - Add Shape#setFill 168 | 169 | - vertex() can teke UV parameters 170 | - vertex() records the fill color 171 | - Drawing shapes with texture is affected by tin() instead of the fill() 172 | 173 | 174 | ## [v0.5.31] - 2023-12-09 175 | 176 | - Add Shape class 177 | - Add createShape(), shape(), shapeMode() 178 | - Add beginShape(), endShape(), and vertex(x, y) 179 | - Test with p5.rb 180 | - GraphicsContext#scale() can take z parameter 181 | - Set default miter_limit to 10 182 | - Trigger github actions on all pull_request 183 | 184 | 185 | ## [v0.5.30] - 2023-11-09 186 | 187 | - Test drawing methods with p5.rb 188 | - Add .github/workflows/test-draw.yml 189 | - Use Gemfile to install gems for development instead of add_development_dependency in gemspec 190 | 191 | 192 | ## [v0.5.29] - 2023-10-29 193 | 194 | - Update dependencies 195 | 196 | 197 | ## [v0.5.28] - 2023-10-25 198 | 199 | - Add GraphicsContext#clear 200 | - Add Graphics#save 201 | - save() returns nil 202 | 203 | 204 | ## [v0.5.27] - 2023-07-30 205 | 206 | - Update dependencies 207 | 208 | 209 | ## [v0.5.26] - 2023-07-30 210 | 211 | - add windowOrientation() 212 | 213 | 214 | ## [v0.5.25] - 2023-07-21 215 | 216 | - Timer block can call drawing methods 217 | - Add Processing::Window#update_window, and delete RubySketch::Window class 218 | 219 | 220 | ## [v0.5.24] - 2023-07-11 221 | 222 | - Resize the canvas when the window is resized 223 | 224 | 225 | ## [v0.5.23] - 2023-07-11 226 | 227 | - Update dependencies 228 | 229 | 230 | ## [v0.5.22] - 2023-07-10 231 | 232 | - Update dependencies 233 | 234 | 235 | ## [v0.5.21] - 2023-07-09 236 | 237 | - calling setup() without block does nothing 238 | 239 | 240 | ## [v0.5.20] - 2023-06-27 241 | 242 | - Update dependencies 243 | 244 | 245 | ## [v0.5.19] - 2023-06-22 246 | 247 | - Update dependencies 248 | 249 | 250 | ## [v0.5.18] - 2023-06-11 251 | 252 | - mousePressed, mouseReleased, mouseMoved, mouseDragged, mouseClicked ignore multiple touches 253 | - Fix that pointer event handles only the first pointer’s type and ignoring rest pointer's types 254 | 255 | 256 | ## [v0.5.17] - 2023-06-07 257 | 258 | - Add Image#set() and Image#get() 259 | - Add color(), red(), green(), blue(), and alpha() 260 | - Add lerpColor() 261 | - Add focused() 262 | 263 | 264 | ## [v0.5.16] - 2023-06-02 265 | 266 | - Fix failed tests and add tests 267 | 268 | 269 | ## [v0.5.15] - 2023-06-02 270 | 271 | - Set initial canvas size to same as the window size 272 | - Use WIDTH and HEIGHT env vars for initial canvas size 273 | - Shader class can take shadertoy frament shader source 274 | - createGraphics() can take pixelDensity parameter 275 | - Pass self to the block call of beginDraw(), and ensure endDraw 276 | 277 | 278 | ## [v0.5.14] - 2023-05-29 279 | 280 | - Add windowMove() and windowResize() 281 | - Add windowMoved(), windowResized(), and windowResizable() 282 | - Add windowX() and windowY() 283 | - Add displayWidth(), displayHeight(), pixelWidth(), pixelHeight(), and pixelDensity() 284 | - Add doc for width() and height() 285 | - Fix crash on calling size() 286 | 287 | 288 | ## [v0.5.13] - 2023-05-27 289 | 290 | - pushMatrix(), pushStyle(), and push() return the result of the expression at the end of the block when a block given 291 | - required_ruby_version >= 3.0.0 292 | - Add spec.license 293 | 294 | 295 | ## [v0.5.12] - 2023-05-26 296 | 297 | - pushStyle/popStyle do not manage filter state 298 | 299 | 300 | ## [v0.5.11] - 2023-05-21 301 | 302 | - copy() and blend() now work with tint color 303 | 304 | 305 | ## [v0.5.10] - 2023-05-19 306 | 307 | - Vector#array takes parameter for number of dimensions 308 | 309 | 310 | ## [v0.5.9] - 2023-05-18 311 | 312 | - Update dependencies 313 | 314 | 315 | ## [v0.5.8] - 2023-05-13 316 | 317 | - Update dependencies 318 | 319 | 320 | ## [v0.5.7] - 2023-05-11 321 | 322 | - Add examples/shake.rb 323 | - Fix Vector.random2D() not working correctly when angleMode is set to DEGREES 324 | 325 | 326 | ## [v0.5.6] - 2023-05-08 327 | 328 | - Add inspect() to classes 329 | - Alias draw methods 330 | - colorMode(), angleMode(), and blendMode() returns current mode value 331 | - fix that mouseButton returns 0 on mouseReleased 332 | 333 | 334 | ## [v0.5.5] - 2023-04-30 335 | 336 | - Update documents 337 | 338 | 339 | ## [v0.5.4] - 2023-04-25 340 | 341 | - Update reflex to v0.1.34 342 | 343 | 344 | ## [v0.5.3] - 2023-04-22 345 | 346 | - Delete RubyProcessing.podspec 347 | - Do not depend on Beeps 348 | - If there are no user blocks, the window is not displayed and exits 349 | 350 | 351 | ## [v0.5.2] - 2023-03-02 352 | 353 | - delete rubysketch.rb and rubysketch-processing.rb 354 | 355 | 356 | ## [v0.5.1] - 2023-03-01 357 | 358 | - requiring 'rubysketch-processing' is deprecated 359 | 360 | 361 | ## [v0.5.0] - 2023-02-09 362 | 363 | - requiring 'processing/include' is deprecated 364 | - require 'processing' and 'using Processing' is now required 365 | - do not show the window if a draw block is not given 366 | 367 | 368 | ## [v0.4.0] - 2022-12-29 369 | 370 | - renamed from rubysketch.gem to processing.gem 371 | - renamed from RubySketch Pod to RubyProcessing Pod 372 | - delete glsl mode 373 | 374 | 375 | ## [v0.3.22] - 2022-11-14 376 | 377 | RubySketch::Processing 378 | - add Shader class 379 | - add shader(), resetShader(), createShader(), loadShader(), and filter() 380 | - update the pixel density of context if the screen pixel density is changed 381 | - setUniform() can also take array of numbers, vector, and texture image. 382 | - pushStyle() manages states of textAlign, tint, and filter 383 | - push/pushMatrix/pushStyle call pop() on ensure 384 | 385 | RubySketch::GLSL 386 | - displays with pixel density 1.0 387 | 388 | 389 | ## [v0.3.21] - 2022-09-05 390 | 391 | - add rubysketch-glsl.rb 392 | - add blend(), createImage(), setTitle(), tint() and noTint() 393 | - add save() that saves screen image to file 394 | - circle() function is affected by ellipseMode() 395 | - point() draws by line(x, y, x, y) 396 | - change initial values for strokeCap and strokeJoin to ROUND and MITER 397 | 398 | 399 | ## [v0.3.20] - 2022-07-24 400 | 401 | - add mouseClicked() 402 | - add blendMode() 403 | - add clip() and noClip() 404 | - translate() can take 'z' parameter 405 | - fix that resizing canvas consumes too much memory 406 | 407 | 408 | ## [v0.3.19] - 2021-12-5 409 | 410 | - fix runtime error 411 | 412 | 413 | ## [v0.3.18] - 2021-12-5 414 | 415 | - add 'mouseButton' 416 | - pointer cancel event calls pointer_up block 417 | 418 | 419 | ## [v0.3.17] - 2021-12-5 420 | 421 | - add Touch#id 422 | 423 | 424 | ## [v0.3.16] - 2021-2-14 425 | 426 | - add key, keyCode and keyPressed system values 427 | - add keyPressed(), keyReleased() and keyTyped() functions 428 | - add motionGravity value and motion() function 429 | 430 | 431 | ## [v0.3.15] - 2020-12-12 432 | 433 | - delete temporary directory on launch 434 | 435 | 436 | ## [v0.3.14] - 2020-12-12 437 | 438 | - fix loadImage() fails when Encoding.default_internal is not nil 439 | 440 | 441 | ## [v0.3.13] - 2020-12-12 442 | 443 | - size(), createCanvas(): default pixelDensity is same as current value 444 | 445 | 446 | ## [v0.3.12] - 2020-12-10 447 | 448 | - size() and createCanvas() take 'pixelDensity' parameter and default value is 1 449 | 450 | 451 | ## [v0.3.11] - 2020-12-9 452 | 453 | - add size(), createCanvas() and pixelDensity() 454 | 455 | 456 | ## [v0.3.10] - 2020-12-1 457 | 458 | - invert angle parameter value for arc() to fix compatibility to processing API 459 | 460 | 461 | ## [v0.3.9] - 2020-11-30 462 | 463 | - Graphics#beginDraw() can take block to call endDraw automatically 464 | - Capture#start() always returns nil 465 | - add delay_camera.rb 466 | 467 | 468 | ## [v0.3.8] - 2020-11-27 469 | 470 | - Capture#initialize() can take requestWidth, requestHeight and cameraName 471 | - add Capture#width and Capture#height 472 | 473 | 474 | ## [v0.3.7] - 2020-11-18 475 | 476 | - add Capture class 477 | - add log(), exp(), sqrt() and PI 478 | - add examples/camera.rb 479 | - add examples/breakout.rb 480 | - fix error on calling image() 481 | 482 | 483 | ## [v0.3.6] - 2020-08-02 484 | 485 | - random() can take array or nothing 486 | - use github actions to release gem package 487 | 488 | 489 | ## [v0.3.5] - 2020-08-02 490 | 491 | - add random() 492 | - add sin(), cos(), tan(), asin(), acos(), atan() and atan2() 493 | - make Vector class accessible from user script context 494 | - fix error on calling rotate() 495 | 496 | 497 | ## [v0.3.4] - 2020-08-02 498 | 499 | - delete Utility module 500 | 501 | 502 | ## [v0.3.3] - 2020-08-01 503 | 504 | - add Vector class 505 | 506 | 507 | ## [v0.3.2] - 2020-07-22 508 | 509 | - text() draws to the baseline by default 510 | - add textWidth(), textAscent(), textDescent() and textAlign() 511 | - change initial color for fill() and stroke() 512 | - change initial background color to grayscale 0.8 513 | 514 | 515 | ## [v0.3.1] - 2020-07-17 516 | 517 | - add touchStarted(), touchEnded(), touchMoved() and touches() 518 | - make all event handler drawable 519 | - limit font max size to 256 520 | 521 | 522 | ## [v0.3.0] - 2020-05-21 523 | 524 | - add createGraphics() 525 | 526 | 527 | ## [v0.2.7] - 2020-04-17 528 | 529 | - add strokeCap() and strokeJoin() 530 | 531 | 532 | ## [v0.2.6] - 2020-04-17 533 | 534 | - push(), pushMatrix() and pushStyle() take block to automatic pop 535 | - refine startup process 536 | - add curve() and bezier() 537 | - add imageMode(), Image#resize(), Image#copy() and copy() 538 | - add loop(), noLoop() and redraw() 539 | 540 | 541 | ## [v0.2.5] - 2020-03-29 542 | 543 | - delete debug prints 544 | - show unsupported image type 545 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rexml' 4 | gem 'rake' 5 | gem 'test-unit' 6 | gem 'yard' 7 | gem 'ferrum' 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.5) 5 | public_suffix (>= 2.0.2, < 6.0) 6 | concurrent-ruby (1.2.2) 7 | ferrum (0.14) 8 | addressable (~> 2.5) 9 | concurrent-ruby (~> 1.1) 10 | webrick (~> 1.7) 11 | websocket-driver (>= 0.6, < 0.8) 12 | power_assert (2.0.3) 13 | public_suffix (5.0.3) 14 | rake (13.1.0) 15 | test-unit (3.6.1) 16 | power_assert 17 | webrick (1.7.0) 18 | websocket-driver (0.7.6) 19 | websocket-extensions (>= 0.1.0) 20 | websocket-extensions (0.1.5) 21 | yard (0.9.34) 22 | 23 | PLATFORMS 24 | arm64-darwin-20 25 | arm64-darwin-21 26 | 27 | DEPENDENCIES 28 | ferrum 29 | rake 30 | test-unit 31 | yard 32 | 33 | BUNDLED WITH 34 | 2.3.3 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 xord.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Processing for CRuby - Processing compatible Creative Coding Framework 2 | 3 | ![License](https://img.shields.io/github/license/xord/processing) 4 | ![Build Status](https://github.com/xord/processing/actions/workflows/test.yml/badge.svg) 5 | ![Gem Version](https://badge.fury.io/rb/processing.svg) 6 | 7 | ## ⚠️ Notice 8 | 9 | This repository is a read-only mirror of our monorepo. 10 | We do not accept pull requests or direct contributions here. 11 | 12 | ### 🔄 Where to Contribute? 13 | 14 | All development happens in our [xord/all](https://github.com/xord/all) monorepo, which contains all our main libraries. 15 | If you'd like to contribute, please submit your changes there. 16 | 17 | For more details, check out our [Contribution Guidelines](./CONTRIBUTING.md). 18 | 19 | Thanks for your support! 🙌 20 | 21 | ## 🚀 About 22 | 23 | **Processing for CRuby** is a creative coding framework compatible with the Processing language, designed specifically for CRuby. 24 | 25 | It allows artists and developers to create visual art and interactive graphics using the familiar Processing syntax and concepts, leveraging the power and flexibility of Ruby. 26 | 27 | ## 📦 Installation 28 | 29 | Add this line to your Gemfile: 30 | ```ruby 31 | $ gem 'processing' 32 | ``` 33 | 34 | Then, install gem: 35 | ```bash 36 | $ bundle install 37 | ``` 38 | 39 | Or install it directly: 40 | ```bash 41 | $ gem install processing 42 | ``` 43 | 44 | ## 📜 License 45 | 46 | **Processing for CRuby** is licensed under the MIT License. 47 | See the [LICENSE](./LICENSE) file for details. 48 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | 3 | %w[../xot ../rucy ../rays ../reflex .] 4 | .map {|s| File.expand_path "#{s}/lib", __dir__} 5 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 6 | 7 | require 'rucy/rake' 8 | 9 | require 'xot/extension' 10 | require 'rucy/extension' 11 | require 'rays/extension' 12 | require 'reflex/extension' 13 | require 'processing/extension' 14 | 15 | 16 | def test_with_browser() 17 | ENV['TEST_WITH_BROWSER'] = '1' 18 | end 19 | 20 | EXTENSIONS = [Xot, Rucy, Rays, Reflex, Processing] 21 | 22 | ENV['RDOC'] = 'yardoc --no-private' 23 | 24 | #test_with_browser if ci? 25 | 26 | default_tasks 27 | use_bundler 28 | test_ruby_extension unless github_actions? && win32? 29 | generate_documents 30 | build_ruby_gem 31 | 32 | task :clean => 'test:clean' 33 | 34 | namespace :test do 35 | task :clean do 36 | sh %( rm -rf test/.png/*.png ) 37 | end 38 | 39 | task :with_browser do 40 | test_with_browser 41 | end 42 | 43 | ::Rake::TestTask.new :draw do |t| 44 | t.test_files = FileList['test/test_*.rb'] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.12 -------------------------------------------------------------------------------- /examples/breakout.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | 9 | PADDING = 50 10 | BRICK_COUNT = 10 11 | 12 | $objs = [] 13 | $bar = nil 14 | $gameover = false 15 | 16 | setup do 17 | addWalls 18 | addBricks 19 | 20 | colorMode HSB, 360, 100, 100 21 | ellipseMode CORNER 22 | background 0 23 | noStroke 24 | fill 0, 0, 100 25 | end 26 | 27 | draw do 28 | background 0 29 | $objs.each do |o| 30 | o.update 31 | o.draw 32 | end 33 | drawTexts 34 | end 35 | 36 | mousePressed do 37 | start unless started? 38 | end 39 | 40 | mouseDragged do 41 | $bar.pos.x = mouseX - $bar.w / 2 if $bar 42 | end 43 | 44 | def start() 45 | $bar = Bar.new 46 | $objs += [$bar, Ball.new($bar)] 47 | end 48 | 49 | def started?() 50 | $bar 51 | end 52 | 53 | def gameover?() 54 | started? && 55 | $objs.count {|o| o.kind_of? Ball} == 0 56 | end 57 | 58 | def cleared?() 59 | started? && !gameover? && 60 | $objs.count {|o| o.kind_of? Brick} == 0 61 | end 62 | 63 | def addWalls() 64 | left = Obj.new 0, 0, 10, height 65 | top = Obj.new 0, 0, width, 10 66 | right = Obj.new width - 10, 0, 10, height 67 | bottom = Bottom.new 0, height - 10, width, 10 68 | $objs += [top, bottom, left, right] 69 | end 70 | 71 | def addBricks() 72 | brickW = (width - PADDING * 2) / BRICK_COUNT 73 | 5.times do |y| 74 | BRICK_COUNT.times do |x| 75 | xx = PADDING + brickW * x 76 | yy = PADDING + 30 * y 77 | $objs.push Brick.new(xx, yy, brickW - 5, 20, y * 60) 78 | end 79 | end 80 | end 81 | 82 | def drawTexts() 83 | push do 84 | textAlign CENTER, CENTER 85 | 86 | if !started? 87 | fill 50, 100, 100 88 | textSize 50 89 | text "BREAKOUT", 0, 0, width, height 90 | textSize 20 91 | translate 0, 100 92 | text "Tap to start!", 0, 0, width, height 93 | elsif cleared? 94 | fill 100, 80, 100 95 | textSize 50 96 | text "CLEAR!", 0, 0, width, height 97 | elsif gameover? 98 | fill 0, 20, 100 99 | textSize 50 100 | text "GAMEOVER", 0, 0, width, height 101 | end 102 | end 103 | end 104 | 105 | class Obj 106 | attr_reader :pos, :w, :h, :vel 107 | 108 | def initialize(x, y, w, h, vx = 0, vy = 0) 109 | @pos = createVector x, y 110 | @w, @h = w, h 111 | @vel = createVector vx, vy 112 | end 113 | 114 | def update() 115 | @pos.add @vel 116 | end 117 | 118 | def draw() 119 | rect @pos.x, @pos.y, @w, @h 120 | end 121 | 122 | def bounds() 123 | x, y = @pos.x, @pos.y 124 | return x, y, x + @w, y + @h 125 | end 126 | 127 | def center() 128 | createVector @pos.x + @w / 2, @pos.y + @h / 2 129 | end 130 | end 131 | 132 | class Ball < Obj 133 | def initialize(bar) 134 | super bar.pos.x, bar.pos.y - bar.h, 20, 20 135 | self.vel = createVector random(-1, 1), -1 136 | end 137 | 138 | def vel=(v) 139 | @vel = v.dup.normalize.mult 8 140 | end 141 | 142 | def update() 143 | b = bounds.dup 144 | super 145 | checkHit b 146 | end 147 | 148 | def checkHit(prevBounds) 149 | x1, y1, x2, y2 = prevBounds 150 | hitH = hitV = false 151 | hits = [] 152 | $objs.each do |o| 153 | next if o == self 154 | next unless intersect? o 155 | hits.push o 156 | ox1, oy1, ox2, oy2 = o.bounds 157 | hitH ||= !overlap?(x1, x2, ox1, ox2) 158 | hitV ||= !overlap?(y1, y2, oy1, oy2) 159 | end 160 | vel.x *= -1 if hitH 161 | vel.y *= -1 if hitV 162 | 163 | hits.each {|o| hit o} 164 | end 165 | 166 | def intersect?(o) 167 | x1, y1, x2, y2 = bounds 168 | ox1, oy1, ox2, oy2 = o.bounds 169 | overlap?(x1, x2, ox1, ox2) && overlap?(y1, y2, oy1, oy2) 170 | end 171 | 172 | def overlap?(a1, a2, b1, b2) 173 | a1 <= b2 && b1 <= a2 174 | end 175 | 176 | def hit(o) 177 | case o 178 | when Bar then self.vel = center.sub o.center 179 | when Brick then $objs.delete o 180 | when Bottom then $objs.delete self unless cleared? 181 | end 182 | end 183 | end 184 | 185 | class Bar < Obj 186 | def initialize() 187 | w = 100 188 | super (width - w) / 2, height - 50, w, 20 189 | end 190 | end 191 | 192 | class Brick < Obj 193 | def initialize(x, y, w, h, hue) 194 | super x, y, w, h 195 | @hue = hue 196 | end 197 | 198 | def draw() 199 | push do 200 | fill @hue, 50, 100 201 | super 202 | end 203 | end 204 | end 205 | 206 | class Bottom < Obj 207 | def draw() 208 | push do 209 | fill 0, 0, 50 210 | super 211 | end 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /examples/camera.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | 9 | cam = Capture.new 300, 300 10 | cam.start 11 | 12 | draw do 13 | background 0 14 | image cam, 0, 0 15 | end 16 | -------------------------------------------------------------------------------- /examples/clock.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | 9 | COLORS = %w[ #F99292 #FFBC61 #FFC679 #FFF4E0 ] 10 | 11 | def now () 12 | Time.now.to_f 13 | end 14 | 15 | start = now 16 | 17 | setup do 18 | colorMode RGB, 1 19 | angleMode DEGREES 20 | end 21 | 22 | draw do 23 | background 0 24 | 25 | pushMatrix do 26 | translate width / 2, height / 2 27 | 28 | pushMatrix do 29 | fill COLORS[0] 30 | ellipse 0, 0, 20, 20 31 | rotate (now - start) / 60.0 * 360 32 | stroke COLORS[0] 33 | strokeWeight 5 34 | line 0, 0, 200, 0 35 | fill 1 36 | end 37 | 38 | pushMatrix do 39 | strokeWeight 3 40 | 60.times do 41 | rotate 6 42 | stroke COLORS[1] 43 | line 200, 0, 210, 0 44 | end 45 | end 46 | 47 | pushMatrix do 48 | strokeWeight 5 49 | 12.times do 50 | rotate 30 51 | stroke COLORS[3] 52 | line 190, 0, 210, 0 53 | end 54 | end 55 | end 56 | 57 | textSize 20 58 | text "#{frameRate.to_i} FPS", 10, 10 59 | end 60 | -------------------------------------------------------------------------------- /examples/delay_camera.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | 9 | w, h = width, height 10 | 11 | cam = Capture.new w, h, Capture.list.last 12 | cam.start 13 | 14 | images = 60.times.map { 15 | Graphics.new w, h 16 | } 17 | 18 | draw do 19 | if frameCount % 2 == 0 20 | images.unshift images.pop 21 | images.first.tap do |image| 22 | image.beginDraw { 23 | image.image cam, 0, 0 24 | } 25 | end 26 | end 27 | 28 | background 0 29 | segment_h= h / images.size 30 | images.each.with_index do |image, i| 31 | y = i * segment_h 32 | copy image, 0, y, w, segment_h, 0, y, w, segment_h 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/filter.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | 9 | icon = loadImage 'https://xord.org/rubysketch/images/rubysketch128.png' 10 | 11 | draw do 12 | background 0, 10 13 | image icon, 100, 100 14 | filter INVERT 15 | end 16 | -------------------------------------------------------------------------------- /examples/hello.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | 9 | draw do 10 | background 0, 10 11 | textSize 50 12 | text 'hello, world!', mouseX, mouseY 13 | end 14 | -------------------------------------------------------------------------------- /examples/image.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | 9 | icon = loadImage 'https://xord.org/rubysketch/images/rubysketch128.png' 10 | 11 | draw do 12 | background 0, 10 13 | image icon, mouseX, mouseY, icon.width / 10, icon.height / 10 14 | end 15 | -------------------------------------------------------------------------------- /examples/shake.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | shake = 0 9 | 10 | draw do 11 | background 100 12 | 13 | v = Vector.random2D * shake 14 | translate v.x, v.y 15 | shake *= 0.8 16 | 17 | textSize 50 18 | text 'hello, world!', 30, 50 19 | rect 100, 100, 200, 100 20 | end 21 | 22 | mouseClicked do 23 | shake = 10 24 | end 25 | -------------------------------------------------------------------------------- /examples/shapes.rb: -------------------------------------------------------------------------------- 1 | %w[xot rays reflex processing] 2 | .map {|s| File.expand_path "../../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'processing' 6 | using Processing 7 | 8 | 9 | setup do 10 | colorMode RGB, 1 11 | angleMode DEGREES 12 | end 13 | 14 | draw do 15 | background 0 16 | 17 | fill 1 18 | stroke 1, 0.5, 0.2 19 | 20 | translate 10, 10 21 | 22 | push 23 | 24 | text 'point', 0, 0 25 | point 0, 30 26 | 27 | translate 0, 100 28 | 29 | text 'point with strokeWeight', 0, 0 30 | strokeWeight 10 31 | point 0, 30 32 | strokeWeight 0 33 | 34 | translate 0, 100 35 | 36 | text 'line', 0, 0 37 | line 0, 30, 100, 50 38 | 39 | translate 0, 100 40 | 41 | text 'line with strokeWeight (very slow)', 0, 0 42 | strokeWeight 10 43 | line 0, 30, 100, 50 44 | strokeWeight 1 45 | 46 | translate 0, 100 47 | 48 | text 'rect with rectMode(CORNER)', 0, 0 49 | rectMode CORNER 50 | rect 20, 30, 100, 50 51 | 52 | translate 0, 100 53 | 54 | text 'rect with rectMode(CORNERS)', 0, 0 55 | rectMode CORNERS 56 | rect 20, 30, 120, 80 57 | 58 | translate 0, 100 59 | 60 | text 'rect with rectMode(CENTER)', 0, 0 61 | rectMode CENTER 62 | rect 70, 55, 100, 50 63 | 64 | translate 0, 100 65 | 66 | text 'rect with rectMode(RADIUS)', 0, 0 67 | rectMode RADIUS 68 | rect 70, 55, 50, 25 69 | 70 | pop 71 | translate 200, 0 72 | push 73 | 74 | text 'circle', 0, 0 75 | circle 70, 55, 25 76 | 77 | translate 0, 100 78 | 79 | text 'arc', 0, 0 80 | arc 70, 55, 100, 50, 45, 270 81 | 82 | translate 0, 100 83 | 84 | text 'square', 0, 0 85 | square 20, 30, 50 86 | 87 | translate 0, 100 88 | 89 | text 'triangle', 0, 0 90 | triangle 70, 30, 120, 80, 20, 80 91 | 92 | translate 0, 100 93 | 94 | text 'quad', 0, 0 95 | quad 20, 30, 120, 30, 150, 80, 50, 80 96 | 97 | translate 0, 100 98 | 99 | text 'ellipse with ellipseMode(CORNER)', 0, 0 100 | ellipseMode CORNER 101 | ellipse 20, 30, 100, 50 102 | 103 | translate 0, 100 104 | 105 | text 'ellipse with ellipseMode(CORNERS)', 0, 0 106 | ellipseMode CORNERS 107 | ellipse 20, 30, 120, 80 108 | 109 | translate 0, 100 110 | 111 | text 'ellipse with ellipseMode(CENTER)', 0, 0 112 | ellipseMode CENTER 113 | ellipse 70, 55, 100, 50 114 | 115 | translate 0, 100 116 | 117 | text 'ellipse with ellipseMode(RADIUS)', 0, 0 118 | ellipseMode RADIUS 119 | ellipse 70, 55, 50, 25 120 | 121 | pop 122 | end 123 | -------------------------------------------------------------------------------- /lib/processing.rb: -------------------------------------------------------------------------------- 1 | require 'processing/all' 2 | 3 | 4 | module Processing 5 | WINDOW__, CONTEXT__ = Processing.setup__ Processing 6 | 7 | refine Object do 8 | context = CONTEXT__ 9 | Processing.funcs__(context).each do |func| 10 | define_method func do |*args, **kwargs, &block| 11 | context.__send__ func, *args, **kwargs, &block 12 | end 13 | end 14 | end 15 | end# Processing 16 | 17 | 18 | def Processing(snake_case: false) 19 | return Processing unless snake_case 20 | 21 | $processing_refinements_with_snake_case ||= Module.new do 22 | Processing.alias_snake_case_methods__ Processing 23 | 24 | refine Object do 25 | context = Processing::CONTEXT__ 26 | Processing.funcs__(context).each do |func| 27 | define_method func do |*args, **kwargs, &block| 28 | context.__send__ func, *args, **kwargs, &block 29 | end 30 | end 31 | end 32 | end 33 | end 34 | 35 | 36 | begin 37 | w, c = Processing::WINDOW__, Processing::CONTEXT__ 38 | 39 | c.class.constants 40 | .reject {_1 =~ /__$/} 41 | .each {self.class.const_set _1, c.class.const_get(_1)} 42 | 43 | w.__send__ :begin_draw 44 | at_exit do 45 | Processing.events__(c).each do |event| 46 | m = begin method event; rescue NameError; nil end 47 | c.__send__(event) {__send__ event} if m 48 | end 49 | 50 | w.__send__ :end_draw 51 | Processing::App.new {w.show}.start if c.hasUserBlocks__ && !$! 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/processing/all.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'strscan' 3 | require 'digest/sha1' 4 | require 'pathname' 5 | require 'tmpdir' 6 | require 'uri' 7 | require 'rexml' 8 | require 'net/http' 9 | require 'xot/inspectable' 10 | require 'reflex' 11 | 12 | 13 | module Processing 14 | 15 | # @private 16 | EVENT_NAMES__ = %i[ 17 | setup draw 18 | keyPressed keyReleased keyTyped 19 | mousePressed mouseReleased mouseMoved mouseDragged 20 | mouseClicked doubleClicked mouseWheel 21 | touchStarted touchEnded touchMoved 22 | windowMoved windowResized motion 23 | ] 24 | 25 | # @private 26 | def self.setup__(namespace) 27 | w = (ENV['WIDTH'] || 500).to_i 28 | h = (ENV['HEIGHT'] || 500).to_i 29 | 30 | window = Processing::Window.new(w, h) {start} 31 | context = namespace::Context.new window 32 | 33 | return window, context 34 | end 35 | 36 | # @private 37 | def self.funcs__(context) 38 | (context.methods - Object.instance_methods) 39 | .reject {_1 =~ /__$/} # methods for internal use 40 | end 41 | 42 | # @private 43 | def self.events__(context) 44 | to_snake_case__(EVENT_NAMES__).flatten.uniq.select {context.respond_to? _1} 45 | end 46 | 47 | # @private 48 | def self.alias_snake_case_methods__(klass, recursive = 1) 49 | to_snake_case__(klass.instance_methods false) 50 | .reject {|camel, snake| camel =~ /__$/} 51 | .reject {|camel, snake| klass.method_defined? snake} 52 | .each {|camel, snake| klass.alias_method snake, camel} 53 | if recursive > 0 54 | klass.constants.map {klass.const_get _1} 55 | .flatten 56 | .select {_1.class == Module || _1.class == Class} 57 | .each {|inner_class| alias_snake_case_methods__ inner_class, recursive - 1} 58 | end 59 | end 60 | 61 | # @private 62 | def self.to_snake_case__(camel_case_names) 63 | camel_case_names.map do |camel| 64 | snake = camel.to_s.gsub(/([a-z])([A-Z])/) {"#{$1}_#{$2.downcase}"} 65 | [camel, snake].map(&:to_sym) 66 | end 67 | end 68 | 69 | end# Processing 70 | 71 | 72 | require 'processing/extension' 73 | require 'processing/app' 74 | require 'processing/window' 75 | 76 | require 'processing/vector' 77 | require 'processing/image' 78 | require 'processing/font' 79 | require 'processing/touch' 80 | require 'processing/shape' 81 | require 'processing/svg' 82 | require 'processing/shader' 83 | require 'processing/capture' 84 | require 'processing/graphics_context' 85 | require 'processing/graphics' 86 | require 'processing/events' 87 | require 'processing/context' 88 | -------------------------------------------------------------------------------- /lib/processing/app.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # @private 5 | class App < Reflex::Application 6 | 7 | def on_motion(e) 8 | Processing.instance_variable_get(:@window)&.on_motion e 9 | end 10 | 11 | end# App 12 | 13 | 14 | end# Processing 15 | -------------------------------------------------------------------------------- /lib/processing/capture.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Camera object. 5 | # 6 | class Capture 7 | 8 | include Xot::Inspectable 9 | 10 | # Returns a list of available camera device names 11 | # 12 | # @return [Array] device name list 13 | # 14 | def self.list() 15 | Rays::Camera.device_names 16 | end 17 | 18 | # Initialize camera object. 19 | # 20 | # @overload Capture.new() 21 | # @overload Capture.new(cameraName) 22 | # @overload Capture.new(requestWidth, requestHeight) 23 | # @overload Capture.new(requestWidth, requestHeight, cameraName) 24 | # 25 | # @param requestWidth [Integer] captured image width 26 | # @param requestHeight [Integer] captured image height 27 | # @param cameraName [String] camera device name 28 | # 29 | def initialize(*args) 30 | width, height, name = 31 | if args.empty? 32 | [-1, -1, nil] 33 | elsif args[0].kind_of?(String) 34 | [-1, -1, args[0]] 35 | elsif args[0].kind_of?(Numeric) && args[1].kind_of?(Numeric) 36 | [args[0], args[1], args[2]] 37 | else 38 | raise ArgumentError 39 | end 40 | @camera = Rays::Camera.new width, height, device_name: name 41 | end 42 | 43 | # Start capturing. 44 | # 45 | # @return [nil] nil 46 | # 47 | def start() 48 | raise "Failed to start capture" unless @camera.start 49 | nil 50 | end 51 | 52 | # Stop capturing. 53 | # 54 | # @return [nil] nil 55 | # 56 | def stop() 57 | @camera.stop 58 | nil 59 | end 60 | 61 | # Returns is the next captured image available? 62 | # 63 | # @return [Boolean] true means object has next frame 64 | # 65 | def available() 66 | @camera.active? 67 | end 68 | 69 | # Reads next frame image 70 | # 71 | def read() 72 | @camera.image 73 | end 74 | 75 | # Returns the width of captured image 76 | # 77 | # @return [Numeric] the width of captured image 78 | # 79 | def width() 80 | @camera.image&.width || 0 81 | end 82 | 83 | # Returns the height of captured image 84 | # 85 | # @return [Numeric] the height of captured image 86 | # 87 | def height() 88 | @camera.image&.height || 0 89 | end 90 | 91 | # Applies an image filter. 92 | # 93 | # overload filter(shader) 94 | # overload filter(type) 95 | # overload filter(type, param) 96 | # 97 | # @param shader [Shader] a fragment shader to apply 98 | # @param type [THRESHOLD, GRAY, INVERT, BLUR] filter type 99 | # @param param [Numeric] a parameter for each filter 100 | # 101 | def filter(*args) 102 | @filter = Shader.createFilter__(*args) 103 | end 104 | 105 | # @private 106 | def getInternal__() 107 | @camera.image || (@dummyImage ||= Rays::Image.new 1, 1) 108 | end 109 | 110 | # @private 111 | def drawImage__(painter, *args, **states) 112 | shader = painter.shader || @filter&.getInternal__ 113 | painter.push shader: shader, **states do |_| 114 | painter.image getInternal__, *args 115 | end 116 | end 117 | 118 | end# Capture 119 | 120 | 121 | end# Processing 122 | -------------------------------------------------------------------------------- /lib/processing/context.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Processing context 5 | # 6 | class Context 7 | 8 | include Xot::Inspectable 9 | include GraphicsContext 10 | 11 | Capture = Processing::Capture 12 | Font = Processing::Font 13 | Graphics = Processing::Graphics 14 | Image = Processing::Image 15 | Shader = Processing::Shader 16 | TextBounds = Processing::TextBounds 17 | Touch = Processing::Touch 18 | Vector = Processing::Vector 19 | 20 | # Portrait for windowOrientation 21 | # 22 | PORTRAIT = :portrait 23 | 24 | # Landscape for windowOrientation 25 | # 26 | LANDSCAPE = :landscape 27 | 28 | # @private 29 | @@rootContext__ = nil 30 | 31 | # @private 32 | @@context__ = nil 33 | 34 | # @private 35 | def self.context__() 36 | @@context__ || @@rootContext__ 37 | end 38 | 39 | # @private 40 | def self.setContext__(context) 41 | @@context__ = context 42 | end 43 | 44 | # @private 45 | def initialize(window) 46 | @@rootContext__ = self 47 | 48 | tmpdir__.tap {|dir| FileUtils.rm_r dir.to_s if dir.directory?} 49 | 50 | @window__ = window 51 | init__( 52 | @window__.canvas.image, 53 | @window__.canvas.painter.paint {background 0.8}) 54 | 55 | @smooth__ = true 56 | @loop__ = true 57 | @redraw__ = false 58 | @frameCount__ = 0 59 | @key__ = nil 60 | @keyCode__ = nil 61 | @keyRepeat__ = false 62 | @keysPressed__ = Set.new 63 | @pointer__ = nil 64 | @pointerPrev__ = nil 65 | @pointersPressed__ = [] 66 | @pointersReleased__ = [] 67 | @touches__ = [] 68 | @motionGravity__ = createVector 0, 0 69 | 70 | @window__.before_draw = proc {beginDraw__} 71 | @window__.after_draw = proc {endDraw__} 72 | @window__.update_canvas = proc {|i, p| updateCanvas__ i, p} 73 | 74 | @window__.instance_variable_set :@context__, self 75 | 76 | # @private 77 | def @window__.draw_screen(painter) 78 | @context__.drawImage__ painter, image__: canvas.render 79 | end 80 | 81 | drawFrame = -> { 82 | begin 83 | push 84 | @drawBlock__.call if @drawBlock__ 85 | ensure 86 | pop 87 | @frameCount__ += 1 88 | end 89 | } 90 | 91 | @window__.draw = proc do |e| 92 | if @loop__ || @redraw__ 93 | @redraw__ = false 94 | drawFrame.call 95 | end 96 | end 97 | 98 | updateKeyStates = -> event, pressed { 99 | set = @keysPressed__ 100 | @key__ = event.chars 101 | @keyCode__ = event.key 102 | @keyRepeat__ = pressed && set.include?(@keyCode__) 103 | pressed ? set.add(@keyCode__) : set.delete(@keyCode__) 104 | } 105 | 106 | mouseButtonMap = { 107 | mouse_left: LEFT, 108 | mouse_right: RIGHT, 109 | mouse_middle: CENTER 110 | } 111 | 112 | updatePointerStates = -> event { 113 | pointer = event.find {|p| p.id == @pointer__&.id} || event.first 114 | if !mousePressed || pointer.id == @pointer__&.id 115 | @pointerPrev__, @pointer__ = @pointer__, pointer.dup 116 | end 117 | @touches__ = event.map {|p| Touch.new(p.id, *p.pos.to_a)} 118 | } 119 | 120 | updatePointersPressedAndReleased = -> event, pressed { 121 | event.map(&:types).flatten 122 | .tap {|types| types.delete :mouse} 123 | .map {|type| mouseButtonMap[type] || type} 124 | .each do |type| 125 | (pressed ? @pointersPressed__ : @pointersReleased__).push type 126 | if !pressed && index = @pointersPressed__.index(type) 127 | @pointersPressed__.delete_at index 128 | end 129 | end 130 | } 131 | 132 | @window__.key_down = proc do |e| 133 | updateKeyStates.call e, true 134 | @keyPressedBlock__&.call 135 | @keyTypedBlock__&.call if @key__ && !@key__.empty? 136 | end 137 | 138 | @window__.key_up = proc do |e| 139 | updateKeyStates.call e, false 140 | @keyReleasedBlock__&.call 141 | end 142 | 143 | @window__.pointer_down = proc do |e| 144 | updatePointerStates.call e 145 | updatePointersPressedAndReleased.call e, true 146 | @mousePressedBlock__&.call if e.any? {|p| p.id == @pointer__.id} 147 | @touchStartedBlock__&.call 148 | end 149 | 150 | @window__.pointer_up = proc do |e| 151 | updatePointerStates.call e 152 | updatePointersPressedAndReleased.call e, false 153 | if e.any? {|p| p.id == @pointer__.id} 154 | @mouseReleasedBlock__&.call 155 | @mouseClickedBlock__&.call if e.click_count > 0 156 | @doubleClickedBlock__&.call if e.click_count == 2 157 | end 158 | @touchEndedBlock__&.call 159 | @pointersReleased__.clear 160 | end 161 | 162 | @window__.pointer_move = proc do |e| 163 | updatePointerStates.call e 164 | mouseMoved = e.drag? ? @mouseDraggedBlock__ : @mouseMovedBlock__ 165 | mouseMoved&.call if e.any? {|p| p.id == @pointer__.id} 166 | @touchMovedBlock__&.call 167 | end 168 | 169 | @window__.wheel = proc do |e| 170 | @mouseWheelBlock__&.call WheelEvent.new(e) 171 | end 172 | 173 | @window__.move = proc do |e| 174 | @windowMovedBlock__&.call 175 | end 176 | 177 | @window__.resize = proc do |e| 178 | @windowResizedBlock__&.call 179 | end 180 | 181 | @window__.motion = proc do |e| 182 | @motionGravity__ = createVector(*e.gravity.to_a(3)) 183 | @motionBlock__&.call 184 | end 185 | end 186 | 187 | # @private 188 | def hasUserBlocks__() 189 | @drawBlock__ || 190 | @keyPressedBlock__ || 191 | @keyReleasedBlock__ || 192 | @keyTypedBlock__ || 193 | @mousePressedBlock__ || 194 | @mouseReleasedBlock__ || 195 | @mouseMovedBlock__ || 196 | @mouseDraggedBlock__ || 197 | @mouseClickedBlock__ || 198 | @doubleClickedBlock__ || 199 | @mouseWheelBlock__ || 200 | @touchStartedBlock__ || 201 | @touchEndedBlock__ || 202 | @touchMovedBlock__ || 203 | @windowMovedBlock__ || 204 | @windowResizedBlock__ || 205 | @motionBlock__ 206 | end 207 | 208 | # Defines setup block. 209 | # 210 | # @return [nil] nil 211 | # 212 | # @see https://processing.org/reference/setup_.html 213 | # @see https://p5js.org/reference/p5/setup/ 214 | # 215 | def setup(&block) 216 | @window__.setup = block if block 217 | nil 218 | end 219 | 220 | # Defines draw block. 221 | # 222 | # @return [nil] nil 223 | # 224 | # @see https://processing.org/reference/draw_.html 225 | # @see https://p5js.org/reference/p5/draw/ 226 | # 227 | def draw(&block) 228 | @drawBlock__ = block if block 229 | nil 230 | end 231 | 232 | # Defines keyPressed block. 233 | # 234 | # @return [Boolean] is any key pressed or not 235 | # 236 | # @see https://processing.org/reference/keyPressed_.html 237 | # @see https://p5js.org/reference/p5/keyPressed/ 238 | # 239 | def keyPressed(&block) 240 | @keyPressedBlock__ = block if block 241 | keyIsPressed 242 | end 243 | 244 | # Defines keyReleased block. 245 | # 246 | # @return [nil] nil 247 | # 248 | # @see https://processing.org/reference/keyReleased_.html 249 | # @see https://p5js.org/reference/p5/keyReleased/ 250 | # 251 | def keyReleased(&block) 252 | @keyReleasedBlock__ = block if block 253 | nil 254 | end 255 | 256 | # Defines keyTyped block. 257 | # 258 | # @return [nil] nil 259 | # 260 | # @see https://processing.org/reference/keyTyped_.html 261 | # @see https://p5js.org/reference/p5/keyTyped/ 262 | # 263 | def keyTyped(&block) 264 | @keyTypedBlock__ = block if block 265 | nil 266 | end 267 | 268 | # Defines mousePressed block. 269 | # 270 | # @return [Boolean] is any mouse button pressed or not 271 | # 272 | # @see https://processing.org/reference/mousePressed_.html 273 | # @see https://processing.org/reference/mousePressed.html 274 | # @see https://p5js.org/reference/p5/mousePressed/ 275 | # 276 | def mousePressed(&block) 277 | @mousePressedBlock__ = block if block 278 | not @pointersPressed__.empty? 279 | end 280 | 281 | # Defines mouseReleased block. 282 | # 283 | # @return [nil] nil 284 | # 285 | # @see https://processing.org/reference/mouseReleased_.html 286 | # @see https://p5js.org/reference/p5/mouseReleased/ 287 | # 288 | def mouseReleased(&block) 289 | @mouseReleasedBlock__ = block if block 290 | nil 291 | end 292 | 293 | # Defines mouseMoved block. 294 | # 295 | # @return [nil] nil 296 | # 297 | # @see https://processing.org/reference/mouseMoved_.html 298 | # @see https://p5js.org/reference/p5/mouseMoved/ 299 | # 300 | def mouseMoved(&block) 301 | @mouseMovedBlock__ = block if block 302 | nil 303 | end 304 | 305 | # Defines mouseDragged block. 306 | # 307 | # @return [nil] nil 308 | # 309 | # @see https://processing.org/reference/mouseDragged_.html 310 | # @see https://p5js.org/reference/p5/mouseDragged/ 311 | # 312 | def mouseDragged(&block) 313 | @mouseDraggedBlock__ = block if block 314 | nil 315 | end 316 | 317 | # Defines mouseClicked block. 318 | # 319 | # @return [nil] nil 320 | # 321 | # @see https://processing.org/reference/mouseClicked_.html 322 | # @see https://p5js.org/reference/p5/mouseClicked/ 323 | # 324 | def mouseClicked(&block) 325 | @mouseClickedBlock__ = block if block 326 | nil 327 | end 328 | 329 | # Defines doubleClicked block. 330 | # 331 | # @return [nil] nil 332 | # 333 | # @see https://p5js.org/reference/p5/doubleClicked/ 334 | # 335 | def doubleClicked(&block) 336 | @doubleClickedBlock__ = block if block 337 | nil 338 | end 339 | 340 | # Defines mouseWheel block. 341 | # 342 | # @return [nil] nil 343 | # 344 | # @see https://processing.org/reference/mouseWheel_.html 345 | # @see https://p5js.org/reference/p5/mouseWheel/ 346 | # 347 | def mouseWheel(&block) 348 | @mouseWheelBlock__ = block if block 349 | nil 350 | end 351 | 352 | # Defines touchStarted block. 353 | # 354 | # @return [nil] nil 355 | # 356 | # @see https://p5js.org/reference/p5/touchStarted/ 357 | # 358 | def touchStarted(&block) 359 | @touchStartedBlock__ = block if block 360 | nil 361 | end 362 | 363 | # Defines touchEnded block. 364 | # 365 | # @return [nil] nil 366 | # 367 | # @see https://p5js.org/reference/p5/touchEnded/ 368 | # 369 | def touchEnded(&block) 370 | @touchEndedBlock__ = block if block 371 | nil 372 | end 373 | 374 | # Defines touchMoved block. 375 | # 376 | # @return [nil] nil 377 | # 378 | # @see https://p5js.org/reference/p5/touchMoved/ 379 | # 380 | def touchMoved(&block) 381 | @touchMovedBlock__ = block if block 382 | nil 383 | end 384 | 385 | # Defines windowMoved block. 386 | # 387 | # @return [nil] nil 388 | # 389 | # @see https://processing.org/reference/windowMoved_.html 390 | # 391 | def windowMoved(&block) 392 | @windowMovedBlock__ = block if block 393 | nil 394 | end 395 | 396 | # Defines windowResized block. 397 | # 398 | # @return [nil] nil 399 | # 400 | # @see https://processing.org/reference/windowResized_.html 401 | # @see https://p5js.org/reference/p5/windowResized/ 402 | # 403 | def windowResized(&block) 404 | @windowResizedBlock__ = block if block 405 | nil 406 | end 407 | 408 | # Defines motion block. 409 | # 410 | # @return [nil] nil 411 | # 412 | def motion(&block) 413 | @motionBlock__ = block if block 414 | nil 415 | end 416 | 417 | # Changes canvas size. 418 | # 419 | # @param width [Integer] new width 420 | # @param height [Integer] new height 421 | # @param pixelDensity [Numeric] new pixel density 422 | # 423 | # @return [nil] nil 424 | # 425 | # @see https://processing.org/reference/size_.html 426 | # 427 | def size(width, height, pixelDensity: self.pixelDensity) 428 | windowResize width, height 429 | resizeCanvas__ width, height, pixelDensity 430 | nil 431 | end 432 | 433 | # Changes canvas size. 434 | # 435 | # @param width [Integer] new width 436 | # @param height [Integer] new height 437 | # @param pixelDensity [Numeric] new pixel density 438 | # 439 | # @return [nil] nil 440 | # 441 | # @see https://p5js.org/reference/p5/createCanvas/ 442 | # 443 | def createCanvas(width, height, pixelDensity: self.pixelDensity) 444 | windowResize width, height 445 | resizeCanvas__ width, height, pixelDensity 446 | nil 447 | end 448 | 449 | # Changes title of window. 450 | # 451 | # @param title [String] new title 452 | # 453 | # @return [nil] nil 454 | # 455 | # @see https://processing.org/reference/setTitle_.html 456 | # 457 | def setTitle(title) 458 | @window__.title = title 459 | nil 460 | end 461 | 462 | # Changes and returns canvas pixel density. 463 | # 464 | # @param density [Numeric] new pixel density 465 | # 466 | # @return [Numeric] current pixel density 467 | # 468 | # @see https://processing.org/reference/pixelDensity_.html 469 | # @see https://p5js.org/reference/p5/pixelDensity/ 470 | # 471 | def pixelDensity(density = nil) 472 | resizeCanvas__ width, height, density if density 473 | @window__.canvas.pixel_density 474 | end 475 | 476 | # Toggles full-screen state or returns the current state. 477 | # 478 | # @param state [Boolean] Whether to display full-screen or not 479 | # 480 | # @return [Boolean] current state 481 | # 482 | # @see https://processing.org/reference/fullScreen_.html 483 | # @see https://p5js.org/reference/p5/fullscreen/ 484 | # 485 | def fullscreen(state = nil) 486 | @window__.fullscreen = state if state != nil 487 | @window__.fullscreen? 488 | end 489 | 490 | alias fullScreen fullscreen 491 | 492 | # Enables anti-aliasing. 493 | # (Anti-aliasing is disabled on high DPI screen) 494 | # 495 | # @return [nil] nil 496 | # 497 | # @see https://processing.org/reference/smooth_.html 498 | # @see https://p5js.org/reference/p5/smooth/ 499 | # 500 | def smooth() 501 | @smooth__ = true 502 | resizeCanvas__ width, height, pixelDensity 503 | nil 504 | end 505 | 506 | # Disables anti-aliasing. 507 | # 508 | # @return [nil] nil 509 | # 510 | # @see https://processing.org/reference/noSmooth_.html 511 | # @see https://p5js.org/reference/p5/noSmooth/ 512 | # 513 | def noSmooth() 514 | @smooth__ = false 515 | resizeCanvas__ width, height, pixelDensity 516 | end 517 | 518 | # @private 519 | def resizeCanvas__(width, height, pixelDensity) 520 | @window__.resize_canvas width, height, pixelDensity, antialiasing: @smooth__ 521 | @window__.auto_resize = false 522 | end 523 | 524 | # Returns the width of the display. 525 | # 526 | # @return [Numeric] width 527 | # 528 | # @see https://processing.org/reference/displayWidth.html 529 | # @see https://p5js.org/reference/p5/displayWidth/ 530 | # 531 | def displayWidth() 532 | @window__.screen.width 533 | end 534 | 535 | # Returns the height of the display. 536 | # 537 | # @return [Numeric] height 538 | # 539 | # @see https://processing.org/reference/displayHeight.html 540 | # @see https://p5js.org/reference/p5/displayHeight/ 541 | # 542 | def displayHeight() 543 | @window__.screen.height 544 | end 545 | 546 | # Returns the pixel density of the display. 547 | # 548 | # @return [Numeric] pixel density 549 | # 550 | # @see https://processing.org/reference/displayDensity_.html 551 | # @see https://p5js.org/reference/p5/displayDensity/ 552 | # 553 | def displayDensity() 554 | @window__.painter.pixel_density 555 | end 556 | 557 | # Move the position of the window. 558 | # 559 | # @param [Numeric] x x position of the window 560 | # @param [Numeric] y y position of the window 561 | # 562 | # @return [nil] nil 563 | # 564 | # @see https://processing.org/reference/windowMove_.html 565 | # 566 | def windowMove(x, y) 567 | @window__.pos = [x, y] 568 | nil 569 | end 570 | 571 | # Sets the size of the window. 572 | # 573 | # @param [Numeric] width width of the window 574 | # @param [Numeric] height height of the window 575 | # 576 | # @return [nil] nil 577 | # 578 | # @see https://processing.org/reference/windowResize_.html 579 | # 580 | def windowResize(width, height) 581 | @window__.size = [width, height] 582 | nil 583 | end 584 | 585 | # Makes the window resizable or not. 586 | # 587 | # @param [Boolean] resizable resizable or not 588 | # 589 | # @return [nil] nil 590 | # 591 | # @see https://processing.org/reference/windowResizable_.html 592 | # 593 | def windowResizable(resizable) 594 | @window__.resizable = resizable 595 | nil 596 | end 597 | 598 | # Sets window orientation mask 599 | # 600 | # @param [PORTRAIT, LANDSCAPE] orientations orientations that window can rotate to 601 | # 602 | # @return [nil] nil 603 | # 604 | def windowOrientation(*orientations) 605 | @window__.orientations = orientations.flatten.uniq 606 | end 607 | 608 | # Returns the x position of the window. 609 | # 610 | # @return [Numeric] horizontal position of the window 611 | # 612 | def windowX() 613 | @window__.x 614 | end 615 | 616 | # Returns the y position of the window. 617 | # 618 | # @return [Numeric] vertical position of the window 619 | # 620 | def windowY() 621 | @window__.y 622 | end 623 | 624 | # Returns the width of the window. 625 | # 626 | # @return [Numeric] window width 627 | # 628 | # @see https://p5js.org/reference/p5/windowWidth/ 629 | # 630 | def windowWidth() 631 | @window__.width 632 | end 633 | 634 | # Returns the height of the window. 635 | # 636 | # @return [Numeric] window height 637 | # 638 | # @see https://p5js.org/reference/p5/windowHeight/ 639 | # 640 | def windowHeight() 641 | @window__.height 642 | end 643 | 644 | # Returns whether the window is active or not. 645 | # 646 | # @return [Boolean] active or not 647 | # 648 | # @see https://processing.org/reference/focused.html 649 | # @see https://p5js.org/reference/p5/focused/ 650 | # 651 | def focused() 652 | @window__.active? 653 | end 654 | 655 | # Returns the number of frames since the program started. 656 | # 657 | # @return [Integer] total number of frames 658 | # 659 | # @see https://processing.org/reference/frameCount.html 660 | # @see https://p5js.org/reference/p5/frameCount/ 661 | # 662 | def frameCount() 663 | @frameCount__ 664 | end 665 | 666 | # Returns the number of frames per second. 667 | # 668 | # @return [Float] frames per second 669 | # 670 | # @see https://processing.org/reference/frameRate.html 671 | # @see https://p5js.org/reference/p5/frameRate/ 672 | # 673 | def frameRate() 674 | @window__.event.fps 675 | end 676 | 677 | # Returns the elapsed time after previous drawing event 678 | # 679 | # @return [Float] elapsed time in milliseconds 680 | # 681 | # @see https://p5js.org/reference/p5/deltaTime/ 682 | # 683 | def deltaTime() 684 | @window__.event.dt * 1000 685 | end 686 | 687 | # Returns the last key that was pressed or released. 688 | # 689 | # @return [String] last key 690 | # 691 | # @see https://processing.org/reference/key.html 692 | # @see https://p5js.org/reference/p5/key/ 693 | # 694 | def key() 695 | @key__ 696 | end 697 | 698 | # Returns the last key code that was pressed or released. 699 | # 700 | # @return [Symbol] last key code 701 | # 702 | # @see https://processing.org/reference/keyCode.html 703 | # @see https://p5js.org/reference/p5/keyCode/ 704 | # 705 | def keyCode() 706 | @keyCode__ 707 | end 708 | 709 | # Returns whether or not any key is pressed. 710 | # 711 | # @return [Boolean] is any key pressed or not 712 | # 713 | # @see https://p5js.org/reference/p5/keyIsPressed/ 714 | # 715 | def keyIsPressed() 716 | not @keysPressed__.empty? 717 | end 718 | 719 | # Returns whether or not the key is currently pressed. 720 | # 721 | # @param keyCode [Numeric] code for the key 722 | # 723 | # @return [Boolean] is the key pressed or not 724 | # 725 | # @see https://p5js.org/reference/p5/keyIsDown/ 726 | # 727 | def keyIsDown(keyCode) 728 | @keysPressed__.include? keyCode 729 | end 730 | 731 | # Returns whether the current key is repeated or not. 732 | # 733 | # @return [Boolean] is the key repeated or not 734 | # 735 | def keyIsRepeated() 736 | @keyRepeat__ 737 | end 738 | 739 | # Returns mouse x position 740 | # 741 | # @return [Numeric] horizontal position of mouse 742 | # 743 | # @see https://processing.org/reference/mouseX.html 744 | # @see https://p5js.org/reference/p5/mouseX/ 745 | # 746 | def mouseX() 747 | @pointer__&.x || 0 748 | end 749 | 750 | # Returns mouse y position 751 | # 752 | # @return [Numeric] vertical position of mouse 753 | # 754 | # @see https://processing.org/reference/mouseY.html 755 | # @see https://p5js.org/reference/p5/mouseY/ 756 | # 757 | def mouseY() 758 | @pointer__&.y || 0 759 | end 760 | 761 | # Returns mouse x position in previous frame 762 | # 763 | # @return [Numeric] horizontal position of mouse 764 | # 765 | # @see https://processing.org/reference/pmouseX.html 766 | # @see https://p5js.org/reference/p5/pmouseX/ 767 | # 768 | def pmouseX() 769 | @pointerPrev__&.x || 0 770 | end 771 | 772 | # Returns mouse y position in previous frame 773 | # 774 | # @return [Numeric] vertical position of mouse 775 | # 776 | # @see https://processing.org/reference/pmouseY.html 777 | # @see https://p5js.org/reference/p5/pmouseY/ 778 | # 779 | def pmouseY() 780 | @pointerPrev__&.y || 0 781 | end 782 | 783 | # Returns which mouse button was pressed 784 | # 785 | # @return [Numeric] LEFT, RIGHT, CENTER or 0 786 | # 787 | # @see https://processing.org/reference/mouseButton.html 788 | # @see https://p5js.org/reference/p5/mouseButton/ 789 | # 790 | def mouseButton() 791 | ((@pointersPressed__ + @pointersReleased__) & [LEFT, RIGHT, CENTER]).last 792 | end 793 | 794 | # Returns array of touches 795 | # 796 | # @return [Array] Touch objects 797 | # 798 | # @see https://p5js.org/reference/p5/touches/ 799 | # 800 | def touches() 801 | @touches__ 802 | end 803 | 804 | # Returns vector for real world gravity 805 | # 806 | # @return [Vector] gravity vector 807 | # 808 | def motionGravity() 809 | @motionGravity__ 810 | end 811 | 812 | # Enables calling draw block on every frame. 813 | # 814 | # @return [nil] nil 815 | # 816 | # @see https://processing.org/reference/loop_.html 817 | # @see https://p5js.org/reference/p5/loop/ 818 | # 819 | def loop() 820 | @loop__ = true 821 | end 822 | 823 | # Disables calling draw block on every frame. 824 | # 825 | # @return [nil] nil 826 | # 827 | # @see https://processing.org/reference/noLoop_.html 828 | # @see https://p5js.org/reference/p5/noLoop/ 829 | # 830 | def noLoop() 831 | @loop__ = false 832 | end 833 | 834 | # Calls draw block to redraw frame. 835 | # 836 | # @return [nil] nil 837 | # 838 | # @see https://processing.org/reference/redraw_.html 839 | # @see https://p5js.org/reference/p5/redraw/ 840 | # 841 | def redraw() 842 | @redraw__ = true 843 | end 844 | 845 | end# Context 846 | 847 | 848 | end# Processing 849 | -------------------------------------------------------------------------------- /lib/processing/events.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Mouse wheel event object. 5 | # 6 | class WheelEvent 7 | 8 | # @private 9 | def initialize(event) 10 | @event = event 11 | end 12 | 13 | def delta() 14 | @event.dy 15 | end 16 | 17 | alias getCount delta 18 | 19 | end# WheelEvent 20 | 21 | 22 | end# Processing 23 | -------------------------------------------------------------------------------- /lib/processing/extension.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # @private 5 | module Extension 6 | 7 | module_function 8 | 9 | def name() 10 | super.split('::')[-2] 11 | end 12 | 13 | def version() 14 | File.read(root_dir 'VERSION')[/[\d\.]+/] 15 | end 16 | 17 | def root_dir(path = '') 18 | File.expand_path "../../#{path}", __dir__ 19 | end 20 | 21 | end# Extension 22 | 23 | 24 | end# Processing 25 | -------------------------------------------------------------------------------- /lib/processing/font.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Font object. 5 | # 6 | # @see https://processing.org/reference/PFont.html 7 | # @see https://p5js.org/reference/p5/p5.Font/ 8 | # 9 | class Font 10 | 11 | # @private 12 | def initialize(font) 13 | @font = font or raise ArgumentError 14 | @cachedSizes = {} 15 | end 16 | 17 | # Returns bounding box. 18 | # 19 | # @overload textBounds(str) 20 | # @overload textBounds(str, x, y) 21 | # @overload textBounds(str, x, y, fontSize) 22 | # 23 | # @param str [String] text to calculate bounding box 24 | # @param x [Numeric] horizontal position of bounding box 25 | # @param y [Numeric] vertical position of bounding box 26 | # @param fontSize [Numeric] font size 27 | # 28 | # @return [TextBounds] bounding box for text 29 | # 30 | # @see https://p5js.org/reference/p5.Font/textBounds/ 31 | # 32 | def textBounds(str, x = 0, y = 0, fontSize = nil) 33 | font = getInternal__ fontSize 34 | TextBounds.new x, y, x + font.width(str), y + font.height 35 | end 36 | 37 | # Returns a string containing a human-readable representation of object. 38 | # 39 | # @return [String] inspected text 40 | # 41 | def inspect() 42 | "#" 43 | end 44 | 45 | # Returns available font names 46 | # 47 | # @return [String] font names 48 | # 49 | # @see https://processing.org/reference/PFont_list_.html 50 | # 51 | def self.list() 52 | Rays::Font.families.values.flatten 53 | end 54 | 55 | # @private 56 | def getInternal__(size = nil) 57 | if size 58 | @cachedSizes[size.to_f] ||= @font.dup.tap {|font| font.size = size} 59 | else 60 | @font 61 | end 62 | end 63 | 64 | # @private 65 | def setSize__(size) 66 | return if size == @font.size 67 | @cachedSizes[@font.size] = @font 68 | @font = getInternal__ size 69 | end 70 | 71 | end# Font 72 | 73 | 74 | # Bounding box for text. 75 | # 76 | class TextBounds 77 | 78 | # Horizontal position 79 | # 80 | attr_reader :x 81 | 82 | # Vertical position 83 | # 84 | attr_reader :y 85 | 86 | # Width of bounding box 87 | # 88 | attr_reader :w 89 | 90 | # Height of bounding box 91 | # 92 | attr_reader :h 93 | 94 | # @private 95 | def initialize(x, y, w, h) 96 | @x, @y, @w, @h = x, y, w, h 97 | end 98 | 99 | # Returns a string containing a human-readable representation of object. 100 | # 101 | # @return [String] inspected text 102 | # 103 | def inspect() 104 | "#" 105 | end 106 | 107 | end# TextBounds 108 | 109 | 110 | end# Processing 111 | -------------------------------------------------------------------------------- /lib/processing/graphics.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Draws graphics into an offscreen buffer 5 | # 6 | # @see https://processing.org/reference/PGraphics.html 7 | # @see https://p5js.org/reference/p5/p5.Graphics/ 8 | # 9 | class Graphics 10 | 11 | include Xot::Inspectable 12 | include GraphicsContext 13 | 14 | # Initialize graphics object. 15 | # 16 | # @see https://p5js.org/reference/p5/p5.Graphics/ 17 | # 18 | def initialize(width, height, pixelDensity = 1) 19 | image = Rays::Image.new( 20 | width, height, Rays::RGBA, pixel_density: pixelDensity) 21 | init__ image, image.painter 22 | end 23 | 24 | # Start drawing. 25 | # 26 | # @see https://processing.org/reference/PGraphics_beginDraw_.html 27 | # 28 | def beginDraw(&block) 29 | @painter__.__send__ :begin_paint 30 | beginDraw__ 31 | push 32 | if block 33 | begin 34 | block.call self 35 | ensure 36 | endDraw 37 | end 38 | end 39 | end 40 | 41 | # End drawing. 42 | # 43 | # @see https://processing.org/reference/PGraphics_endDraw_.html 44 | # 45 | def endDraw() 46 | pop 47 | endDraw__ 48 | @painter__.__send__ :end_paint 49 | end 50 | 51 | end# Graphics 52 | 53 | 54 | end# Processing 55 | -------------------------------------------------------------------------------- /lib/processing/image.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Image object. 5 | # 6 | # @see https://processing.org/reference/PImage.html 7 | # @see https://p5js.org/reference/p5/p5.Image/ 8 | # 9 | class Image 10 | 11 | include Xot::Inspectable 12 | 13 | # @private 14 | def initialize(image) 15 | @image = image 16 | @pixels, @error = nil, false 17 | end 18 | 19 | # Gets width of image. 20 | # 21 | # @return [Numeric] width of image 22 | # 23 | # @see https://processing.org/reference/PImage_width.html 24 | # @see https://p5js.org/reference/p5.Image/width/ 25 | # 26 | def width() 27 | @image&.width || (@error ? -1 : 0) 28 | end 29 | 30 | # Gets height of image. 31 | # 32 | # @return [Numeric] height of image 33 | # 34 | # @see https://processing.org/reference/PImage_height.html 35 | # @see https://p5js.org/reference/p5.Image/height/ 36 | # 37 | def height() 38 | @image&.height || (@error ? -1 : 0) 39 | end 40 | 41 | alias w width 42 | alias h height 43 | 44 | # Returns the width and height of image. 45 | # 46 | # @return [Array] [width, height] 47 | # 48 | def size() 49 | [width, height] 50 | end 51 | 52 | # Sets the color of the pixel. 53 | # 54 | # @param x [Integer] x position of the pixel 55 | # @param y [Integer] y position of the pixel 56 | # @param c [Integer] color value 57 | # 58 | # @return [nil] nil 59 | # 60 | # @see https://processing.org/reference/PImage_set_.html 61 | # @see https://p5js.org/reference/p5.Image/set/ 62 | # 63 | def set(x, y, c) 64 | getInternal__.bitmap(true)[x, y] = self.class.fromColor__(c).map {|n| n / 255.0} 65 | nil 66 | end 67 | 68 | # Returns the color of the pixel. 69 | # 70 | # @return [Integer] color value (0xAARRGGBB) 71 | # 72 | # @see https://processing.org/reference/PImage_get_.html 73 | # @see https://p5js.org/reference/p5.Image/get/ 74 | # 75 | def get(x, y) 76 | getInternal__.bitmap[x, y] 77 | .map {|n| (n * 255).to_i.clamp 0, 255} 78 | .then {|r, g, b, a| self.class.toColor__ r, g, b, a} 79 | end 80 | 81 | # Loads all pixels to the 'pixels' array. 82 | # 83 | # @return [nil] nil 84 | # 85 | # @see https://processing.org/reference/PImage_loadPixels_.html 86 | # @see https://p5js.org/reference/p5.Image/loadPixels/ 87 | # 88 | def loadPixels() 89 | @pixels = getInternal__.pixels 90 | end 91 | 92 | # Update the image pixels with the 'pixels' array. 93 | # 94 | # @return [nil] nil 95 | # 96 | # @see https://processing.org/reference/PImage_updatePixels_.html 97 | # @see https://p5js.org/reference/p5.Image/updatePixels/ 98 | # 99 | def updatePixels() 100 | return unless @pixels 101 | getInternal__.pixels = @pixels 102 | @pixels = nil 103 | end 104 | 105 | # An array of all pixels. 106 | # Call loadPixels() before accessing the array. 107 | # 108 | # @return [Array] color array 109 | # 110 | # @see https://processing.org/reference/PImage_pixels.html 111 | # @see https://p5js.org/reference/p5.Image/pixels/ 112 | # 113 | attr_reader :pixels 114 | 115 | # Applies an image filter. 116 | # 117 | # overload filter(shader) 118 | # overload filter(type) 119 | # overload filter(type, param) 120 | # 121 | # @param shader [Shader] a fragment shader to apply 122 | # @param type [THRESHOLD, GRAY, INVERT, BLUR] filter type 123 | # @param param [Numeric] a parameter for each filter 124 | # 125 | # @see https://processing.org/reference/PImage_filter_.html 126 | # @see https://p5js.org/reference/p5.Image/filter/ 127 | # 128 | def filter(*args) 129 | @filter = Shader.createFilter__(*args) 130 | end 131 | 132 | # Resizes image. 133 | # 134 | # @param width [Numeric] width for resized image 135 | # @param height [Numeric] height for resized image 136 | # 137 | # @return [nil] nil 138 | # 139 | # @see https://processing.org/reference/PImage_resize_.html 140 | # @see https://p5js.org/reference/p5.Image/resize/ 141 | # 142 | def resize(width, height) 143 | @image = Rays::Image.new(width, height).paint do |painter| 144 | painter.image getInternal__, 0, 0, width, height 145 | end 146 | nil 147 | end 148 | 149 | # Copies image. 150 | # 151 | # @overload copy(sx, sy, sw, sh, dx, dy, dw, dh) 152 | # @overload copy(img, sx, sy, sw, sh, dx, dy, dw, dh) 153 | # 154 | # @param img [Image] image for copy source 155 | # @param sx [Numrtic] x position of source region 156 | # @param sy [Numrtic] y position of source region 157 | # @param sw [Numrtic] width of source region 158 | # @param sh [Numrtic] height of source region 159 | # @param dx [Numrtic] x position of destination region 160 | # @param dy [Numrtic] y position of destination region 161 | # @param dw [Numrtic] width of destination region 162 | # @param dh [Numrtic] height of destination region 163 | # 164 | # @return [nil] nil 165 | # 166 | # @see https://processing.org/reference/PImage_copy_.html 167 | # @see https://p5js.org/reference/p5.Image/copy/ 168 | # 169 | def copy(img = nil, sx, sy, sw, sh, dx, dy, dw, dh) 170 | blend img, sx, sy, sw, sh, dx, dy, dw, dh, :normal 171 | end 172 | 173 | # @private 174 | def mask__() 175 | raise NotImplementedError 176 | end 177 | 178 | # Blends image. 179 | # 180 | # @overload blend(sx, sy, sw, sh, dx, dy, dw, dh, mode) 181 | # @overload blend(img, sx, sy, sw, sh, dx, dy, dw, dh, mode) 182 | # 183 | # @param img [Image] image for blend source 184 | # @param sx [Numeric] x position of source region 185 | # @param sy [Numeric] y position of source region 186 | # @param sw [Numeric] width of source region 187 | # @param sh [Numeric] height of source region 188 | # @param dx [Numeric] x position of destination region 189 | # @param dy [Numeric] y position of destination region 190 | # @param dw [Numeric] width of destination region 191 | # @param dh [Numeric] height of destination region 192 | # @param mode [BLEND, ADD, SUBTRACT, LIGHTEST, DARKEST, EXCLUSION, MULTIPLY, SCREEN, REPLACE] blend mode 193 | # 194 | # @return [nil] nil 195 | # 196 | # @see https://processing.org/reference/PImage_blend_.html 197 | # @see https://p5js.org/reference/p5.Image/blend/ 198 | # 199 | def blend(img = nil, sx, sy, sw, sh, dx, dy, dw, dh, mode) 200 | img ||= self 201 | getInternal__.paint do |painter| 202 | img.drawImage__ painter, sx, sy, sw, sh, dx, dy, dw, dh, blend_mode: mode 203 | end 204 | nil 205 | end 206 | 207 | # @private 208 | def blendColor__() 209 | raise NotImplementedError 210 | end 211 | 212 | # @private 213 | def reset__() 214 | raise NotImplementedError 215 | end 216 | 217 | # @private 218 | def getCurrentFrame__() 219 | raise NotImplementedError 220 | end 221 | 222 | # @private 223 | def setFrame__() 224 | raise NotImplementedError 225 | end 226 | 227 | # @private 228 | def numFrames__() 229 | raise NotImplementedError 230 | end 231 | 232 | # @private 233 | def play__() 234 | raise NotImplementedError 235 | end 236 | 237 | # @private 238 | def pause__() 239 | raise NotImplementedError 240 | end 241 | 242 | # @private 243 | def delay__() 244 | raise NotImplementedError 245 | end 246 | 247 | # Saves image to file. 248 | # 249 | # @param filename [String] file name to save image 250 | # 251 | # @return [nil] nil 252 | # 253 | # @see https://processing.org/reference/PImage_save_.html 254 | # @see https://p5js.org/reference/p5.Image/save/ 255 | # 256 | def save(filename) 257 | getInternal__.save filename 258 | nil 259 | end 260 | 261 | # @private 262 | def getInternal__() 263 | @image or raise 'Invalid image object' 264 | end 265 | 266 | # @private 267 | def setInternal__(image, error = false) 268 | @image, @error = image, error 269 | end 270 | 271 | # @private 272 | def drawImage__(painter, *args, **states) 273 | shader = painter.shader || @filter&.getInternal__ 274 | painter.push shader: shader, **states do |_| 275 | painter.image getInternal__, *args 276 | end 277 | end 278 | 279 | # @private 280 | def self.fromColor__(color) 281 | [ 282 | color >> 16 & 0xff, 283 | color >> 8 & 0xff, 284 | color & 0xff, 285 | color >> 24 & 0xff 286 | ] 287 | end 288 | 289 | # @private 290 | def self.toColor__(r, g, b, a) 291 | (r & 0xff) << 16 | 292 | (g & 0xff) << 8 | 293 | (b & 0xff) | 294 | (a & 0xff) << 24 295 | end 296 | 297 | end# Image 298 | 299 | 300 | end# Processing 301 | -------------------------------------------------------------------------------- /lib/processing/shader.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Shader object. 5 | # 6 | # @see https://processing.org/reference/PShader.html 7 | # @see https://p5js.org/reference/p5/p5.Shader/ 8 | # 9 | class Shader 10 | 11 | include Xot::Inspectable 12 | 13 | # Initialize shader object. 14 | # 15 | # @param vertSrc [String] vertex shader source 16 | # @param fragSrc [String] fragment shader source 17 | # 18 | def initialize(vertSrc, fragSrc) 19 | @shader = Rays::Shader.new modifyFragSource__(fragSrc), vertSrc, ENV__ 20 | end 21 | 22 | # Sets uniform variables. 23 | # 24 | # @overload set(name, a) 25 | # @overload set(name, a, b) 26 | # @overload set(name, a, b, c) 27 | # @overload set(name, a, b, c, d) 28 | # @overload set(name, nums) 29 | # @overload set(name, vec) 30 | # @overload set(name, vec, ncoords) 31 | # @overload set(name, tex) 32 | # 33 | # @param name [String] uniform variable name 34 | # @param a [Numeric] int or float value 35 | # @param b [Numeric] int or float value 36 | # @param c [Numeric] int or float value 37 | # @param d [Numeric] int or float value 38 | # @param nums [Array] int or float array 39 | # @param vec [Vector] vector 40 | # @param ncoords [Integer] number of coordinates, max 4 41 | # @param tex [Image] texture image 42 | # 43 | # @see https://processing.org/reference/PShader_set_.html 44 | # @see https://p5js.org/reference/p5.Shader/setUniform/ 45 | # 46 | def set(name, *args) 47 | arg = args.first 48 | case 49 | when Array === arg 50 | @shader.uniform name, *arg 51 | when Numeric === arg 52 | @shader.uniform name, *args 53 | when Vector === arg 54 | vec, ncoords = args 55 | @shader.uniform name, vec.getInternal__.to_a(ncoords || 3) 56 | when arg.respond_to?(:getInternal__) 57 | @shader.uniform name, arg.getInternal__ 58 | else 59 | raise ArgumentError 60 | end 61 | end 62 | 63 | alias setUniform set 64 | 65 | # @private 66 | def getInternal__() 67 | @shader 68 | end 69 | 70 | # @private 71 | ENV__ = { 72 | attribute_position: [:vertex, :position], 73 | attribute_texcoord: :texCoord, 74 | attribute_color: :color, 75 | varying_position: :vertPosition, 76 | varying_texcoord: :vertTexCoord, 77 | varying_color: :vertColor, 78 | uniform_position_matrix: [:transform, :transformMatrix], 79 | uniform_texcoord_matrix: :texMatrix, 80 | uniform_texcoord_min: :texMin, 81 | uniform_texcoord_max: :texMax, 82 | uniform_texcoord_offset: :texOffset, 83 | uniform_texture: [:texMap, :texture] 84 | }.freeze 85 | 86 | # @private 87 | def self.createFilter__(*args) 88 | case arg = args.shift 89 | when Shader 90 | arg 91 | when :threshold 92 | self.new(nil, THRESHOLD_SOURCE__).tap {|sh| sh.set :threshold, (args.shift || 0.5)} 93 | when :gray 94 | self.new nil, GRAY_SOURCE__ 95 | when :invert 96 | self.new nil, INVERT_SOURCE__ 97 | when :blur 98 | self.new(nil, BLUR_SOURCE__).tap {|sh| sh.set :radius, (args.shift || 1).to_f} 99 | else 100 | nil 101 | end 102 | end 103 | 104 | private 105 | 106 | # @private 107 | THRESHOLD_SOURCE__ = <<~END 108 | uniform float threshold; 109 | uniform sampler2D texMap; 110 | varying vec4 vertTexCoord; 111 | varying vec4 vertColor; 112 | void main() { 113 | vec4 col = texture2D(texMap, vertTexCoord.xy) * vertColor; 114 | float gray = col.r * 0.3 + col.g * 0.59 + col.b * 0.11; 115 | gl_FragColor = vec4(vec3(gray > threshold ? 1.0 : 0.0), 1.0); 116 | } 117 | END 118 | 119 | # @private 120 | GRAY_SOURCE__ = <<~END 121 | uniform sampler2D texMap; 122 | varying vec4 vertTexCoord; 123 | varying vec4 vertColor; 124 | void main() { 125 | vec4 col = texture2D(texMap, vertTexCoord.xy); 126 | float gray = col.r * 0.3 + col.g * 0.59 + col.b * 0.11; 127 | gl_FragColor = vec4(vec3(gray), 1.0) * vertColor; 128 | } 129 | END 130 | 131 | # @private 132 | INVERT_SOURCE__ = <<~END 133 | uniform sampler2D texMap; 134 | varying vec4 vertTexCoord; 135 | varying vec4 vertColor; 136 | void main() { 137 | vec4 col = texture2D(texMap, vertTexCoord.xy); 138 | gl_FragColor = vec4(vec3(1.0 - col.rgb), 1.0) * vertColor; 139 | } 140 | END 141 | 142 | # @private 143 | BLUR_SOURCE__ = <<~END 144 | #define PI 3.1415926538 145 | uniform float radius; 146 | uniform sampler2D texMap; 147 | uniform vec3 texMin; 148 | uniform vec3 texMax; 149 | uniform vec3 texOffset; 150 | varying vec4 vertTexCoord; 151 | varying vec4 vertColor; 152 | float gaussian(vec2 pos, float sigma) { 153 | float s2 = sigma * sigma; 154 | return 1.0 / (2.0 * PI * s2) * exp(-(dot(pos, pos) / (2.0 * s2))); 155 | } 156 | void main() { 157 | float sigma = radius * 0.5; 158 | vec3 color = vec3(0.0); 159 | float total_weight = 0.0; 160 | for (float y = -radius; y < radius; y += 1.0) 161 | for (float x = -radius; x < radius; x += 1.0) { 162 | vec2 offset = vec2(x, y); 163 | float weight = gaussian(offset, sigma); 164 | vec2 texcoord = vertTexCoord.xy + offset * texOffset.xy; 165 | if ( 166 | texcoord.x < texMin.x || texMax.x < texcoord.x || 167 | texcoord.y < texMin.y || texMax.y < texcoord.y 168 | ) continue; 169 | color += texture2D(texMap, texcoord).rgb * weight; 170 | total_weight += weight; 171 | } 172 | gl_FragColor = vec4(color / total_weight, 1.0) * vertColor; 173 | } 174 | END 175 | 176 | # @private 177 | def modifyFragSource__(source) 178 | return nil unless source 179 | if hasShadertoyMainImage__?(source) && source !~ /void\s+main\s*\(/ 180 | source += <<~END 181 | varying vec4 vertTexCoord; 182 | void main() { 183 | mainImage(gl_FragColor, vertTexCoord.xy); 184 | } 185 | END 186 | end 187 | { 188 | iTime: :float, 189 | iResolution: :vec2, 190 | iMouse: :vec2 191 | }.each do |uniformName, type| 192 | if needsUniformDeclaration__ type, uniformName, source 193 | source = <<~END + source 194 | uniform #{type} #{uniformName}; 195 | END 196 | end 197 | end 198 | source 199 | end 200 | 201 | # @private 202 | def hasShadertoyMainImage__?(source) 203 | source =~ /void\s+mainImage\s*\(\s*out\s+vec4\s+\w+\s*,\s*in\s+vec2\s+\w+\s*\)/ 204 | end 205 | 206 | # @private 207 | def needsUniformDeclaration__(type, uniformName, source) 208 | source.include?(uniformName.to_s) && 209 | source !~ /uniform\s+#{type}\s+#{uniformName}/ 210 | end 211 | 212 | end# Shader 213 | 214 | 215 | end# Processing 216 | -------------------------------------------------------------------------------- /lib/processing/shape.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Shape object. 5 | # 6 | # @see https://processing.org/reference/PShape.html 7 | # 8 | class Shape 9 | 10 | # @private 11 | def initialize(polygon = nil, children = nil, context: nil) 12 | @polygon, @children = polygon, children 13 | @context = context || Context.context__ 14 | @visible = true 15 | @fill = @stroke = @strokeWeight = @strokeCap = @strokeJoin = @matrix = nil 16 | @type = @points = @curvePoints = @colors = @texcoords = @close = nil 17 | @contours = @contourPoints = @contourColors = @contourTexCoords = nil 18 | end 19 | 20 | # Gets width of shape. 21 | # 22 | # @return [Numeric] width of shape 23 | # 24 | # @see https://processing.org/reference/PShape_width.html 25 | # 26 | def width() 27 | polygon = getInternal__ or return 0 28 | (@bounds ||= polygon.bounds).width 29 | end 30 | 31 | # Gets height of shape. 32 | # 33 | # @return [Numeric] height of shape 34 | # 35 | # @see https://processing.org/reference/PShape_height.html 36 | # 37 | def height() 38 | polygon = getInternal__ or return 0 39 | (@bounds ||= polygon.bounds).height 40 | end 41 | 42 | alias w width 43 | alias h height 44 | 45 | # Returns whether the shape is visible or not. 46 | # 47 | # @return [Boolean] visible or not 48 | # 49 | # @see https://processing.org/reference/PShape_isVisible_.html 50 | # 51 | def isVisible() 52 | @visible 53 | end 54 | 55 | alias visible? isVisible 56 | 57 | # Sets whether to display the shape or not. 58 | # 59 | # @return [nil] nil 60 | # 61 | # @see https://processing.org/reference/PShape_setVisible_.html 62 | # 63 | def setVisible(visible) 64 | @visible = !!visible 65 | nil 66 | end 67 | 68 | # Starts shape data definition. 69 | # 70 | # @return [nil] nil 71 | # 72 | # @see https://processing.org/reference/PShape_beginShape_.html 73 | # 74 | def beginShape(type = nil) 75 | raise "beginShape() cannot be called twice" if drawingShape__ 76 | @fill = @stroke = @strokeWeight = @strokeCap = @strokeJoin = nil 77 | @type = type 78 | @points ||= [] 79 | @curvePoints = [] 80 | @colors ||= [] 81 | @texcoords ||= [] 82 | @close = nil 83 | @contours ||= [] 84 | clearCache__ 85 | nil 86 | end 87 | 88 | # Ends shape data definition. 89 | # 90 | # @return [nil] nil 91 | # 92 | # @see https://processing.org/reference/PShape_endShape_.html 93 | # 94 | def endShape(close = nil) 95 | raise "endShape() must be called after beginShape()" unless drawingShape__ 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 103 | if @close && @curvePoints.size >= 8 104 | x, y = @curvePoints[0, 2] 105 | 2.times {curveVertex x, y} 106 | end 107 | @curvePoints = nil 108 | nil 109 | end 110 | 111 | # Starts a new contour definition. 112 | # 113 | # @return [nil] nil 114 | # 115 | # @see https://processing.org/reference/PShape_beginContour_.html 116 | # 117 | def beginContour() 118 | raise "beginContour() must be called after beginShape()" unless drawingShape__ 119 | @contourPoints, @contourColors, @contourTexCoords = [], [], [] 120 | nil 121 | end 122 | 123 | # Ends contour definition. 124 | # 125 | # @return [nil] nil 126 | # 127 | # @see https://processing.org/reference/PShape_endContour_.html 128 | # 129 | def endContour() 130 | raise "endContour() must be called after beginContour()" unless drawingContour__ 131 | @contours << Rays::Polyline.new( 132 | *@contourPoints, colors: @contourColors, texcoords: @contourTexCoords, 133 | loop: true, hole: true) 134 | @contoursPoints = @contoursColors = @contoursTexCoords = nil 135 | nil 136 | end 137 | 138 | # Append vertex for shape polygon. 139 | # 140 | # @overload vertex(x, y) 141 | # @overload vertex(x, y, u, v) 142 | # 143 | # @param x [Numeric] x position of vertex 144 | # @param y [Numeric] y position of vertex 145 | # @param u [Numeric] u texture coordinate of vertex 146 | # @param v [Numeric] v texture coordinate of vertex 147 | # 148 | # @return [nil] nil 149 | # 150 | # @see https://processing.org/reference/vertex_.html 151 | # @see https://p5js.org/reference/p5/vertex/ 152 | # 153 | def vertex(x, y, u = nil, v = nil) 154 | raise "vertex() must be called after beginShape()" unless drawingShape__ 155 | raise "Either 'u' or 'v' is missing" if (u == nil) != (v == nil) 156 | u ||= x 157 | v ||= y 158 | color = @fill || @context.getPainter__.fill 159 | if drawingContour__ 160 | @contourPoints << x << y 161 | @contourColors << color 162 | @contourTexCoords << u << v 163 | else 164 | @points << x << y 165 | @colors << color 166 | @texcoords << u << v 167 | end 168 | nil 169 | end 170 | 171 | # Append curve vertex for shape polygon. 172 | # 173 | # @param x [Numeric] x position of vertex 174 | # @param y [Numeric] y position of vertex 175 | # 176 | # @return [nil] nil 177 | # 178 | # @see https://processing.org/reference/curveVertex_.html 179 | # @see https://p5js.org/reference/p5/curveVertex/ 180 | # 181 | def curveVertex(x, y) 182 | raise "curveVertex() must be called after beginShape()" unless drawingShape__ 183 | @curvePoints << x << y 184 | if @curvePoints.size >= 8 185 | Rays::Polygon.curve(*@curvePoints[-8, 8]) 186 | .first.to_a.tap {|a| a.shift if @curvePoints.size > 8} 187 | .each {|p| vertex p.x, p.y} 188 | end 189 | nil 190 | end 191 | 192 | # Append bezier vertex for shape polygon. 193 | # 194 | # @param x [Numeric] x position of vertex 195 | # @param y [Numeric] y position of vertex 196 | # 197 | # @return [nil] nil 198 | # 199 | # @see https://processing.org/reference/bezierVertex_.html 200 | # @see https://p5js.org/reference/p5/bezierVertex/ 201 | # 202 | def bezierVertex(x2, y2, x3, y3, x4, y4) 203 | raise "bezierVertex() must be called after beginShape()" unless drawingShape__ 204 | x1, y1 = @points[-2, 2] 205 | raise "vertex() is required before calling bezierVertex()" unless x1 && y1 206 | Rays::Polygon.bezier(x1, y1, x2, y2, x3, y3, x4, y4) 207 | .first.to_a.tap {|a| a.shift} 208 | .each {|p| vertex p.x, p.y} 209 | nil 210 | end 211 | 212 | # Append quadratic vertex for shape polygon. 213 | # 214 | # @param x [Numeric] x position of vertex 215 | # @param y [Numeric] y position of vertex 216 | # 217 | # @return [nil] nil 218 | # 219 | # @see https://processing.org/reference/quadraticVertex_.html 220 | # @see https://p5js.org/reference/p5/quadraticVertex/ 221 | # 222 | def quadraticVertex(cx, cy, x3, y3) 223 | x1, y1 = @points[-2, 2] 224 | raise "vertex() is required before calling quadraticVertex()" unless x1 && y1 225 | bezierVertex( 226 | x1 + (cx - x1) * 2.0 / 3.0, y1 + (cy - y1) * 2.0 / 3.0, 227 | x3 + (cx - x3) * 2.0 / 3.0, y3 + (cy - y3) * 2.0 / 3.0, 228 | x3, y3) 229 | nil 230 | end 231 | 232 | # @private 233 | def drawingShape__() 234 | @curvePoints 235 | end 236 | 237 | # @private 238 | def drawingContour__() 239 | @contourPoints 240 | end 241 | 242 | # Sets fill color. 243 | # 244 | # @overload fill(gray) 245 | # @overload fill(gray, alpha) 246 | # @overload fill(r, g, b) 247 | # @overload fill(r, g, b, alpha) 248 | # 249 | # @param gray [Integer] gray value (0..255) 250 | # @param r [Integer] red value (0..255) 251 | # @param g [Integer] green value (0..255) 252 | # @param b [Integer] blue value (0..255) 253 | # @param alpha [Integer] alpha value (0..255) 254 | # 255 | # @return [nil] nil 256 | # 257 | # @see https://processing.org/reference/fill_.html 258 | # @see https://p5js.org/reference/p5/fill/ 259 | # 260 | def fill(*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) 285 | nil 286 | end 287 | 288 | # Sets the vertex at the index position. 289 | # 290 | # @overload setVertex(index, x, y) 291 | # @overload setVertex(index, vec) 292 | # 293 | # @param index [Integer] the index fo the vertex 294 | # @param x [Numeric] x position of the vertex 295 | # @param y [Numeric] y position of the vertex 296 | # @param vec [Vector] position for the vertex 297 | # 298 | # @return [nil] nil 299 | # 300 | # @see https://processing.org/reference/PShape_setVertex_.html 301 | # 302 | def setVertex(index, point) 303 | return nil unless @points && @points[index * 2, 2]&.size == 2 304 | @points[index * 2, 2] = [point.x, point.y] 305 | nil 306 | end 307 | 308 | # Returns the vertex at the index position. 309 | # 310 | # @param index [Integer] the index fo the vertex 311 | # 312 | # @return [Vector] the vertex position 313 | # 314 | # @see https://processing.org/reference/PShape_getVertex_.html 315 | # 316 | def getVertex(index) 317 | return nil unless @points 318 | point = @points[index * 2, 2] 319 | return nil unless point&.size == 2 320 | @context.createVector(*point) 321 | end 322 | 323 | # Returns the total number of vertices. 324 | # 325 | # @return [Integer] vertex count 326 | # 327 | # @see https://processing.org/reference/PShape_getVertexCount_.html 328 | # 329 | def getVertexCount() 330 | return 0 unless @points 331 | @points.size / 2 332 | end 333 | 334 | # Sets the fill color for all vertices. 335 | # 336 | # @overload setFill(gray) 337 | # @overload setFill(gray, alpha) 338 | # @overload setFill(r, g, b) 339 | # @overload setFill(r, g, b, alpha) 340 | # 341 | # @param gray [Integer] gray value (0..255) 342 | # @param r [Integer] red value (0..255) 343 | # @param g [Integer] green value (0..255) 344 | # @param b [Integer] blue value (0..255) 345 | # @param alpha [Integer] alpha value (0..255) 346 | # 347 | # @return [nil] nil 348 | # 349 | # @see https://processing.org/reference/PShape_setFill_.html 350 | # 351 | def setFill(*args) 352 | fill(*args) 353 | count = getVertexCount 354 | if count > 0 355 | if @colors 356 | @colors.fill @fill 357 | else 358 | @colors = [@fill] * count 359 | end 360 | clearCache__ 361 | elsif @polygon 362 | @polygon = @polygon.transform do |polylines| 363 | polylines.map {|pl| pl.with colors: pl.points.map {@fill}} 364 | end 365 | end 366 | nil 367 | end 368 | 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 422 | end 423 | 424 | # Adds a new child shape. 425 | # 426 | # @param child [Shape] child shape 427 | # @param index [Integer] insert position 428 | # 429 | # @return [nil] nil 430 | # 431 | # @see https://processing.org/reference/PShape_addChild_.html 432 | # 433 | def addChild(child, index = -1) 434 | return unless @children 435 | if index < 0 436 | @children.push child 437 | else 438 | @children.insert index, child 439 | end 440 | nil 441 | end 442 | 443 | # Returns a child shape at the index position. 444 | # 445 | # @param index [Integer] child index 446 | # 447 | # @return [nil] nil 448 | # 449 | # @see https://processing.org/reference/PShape_getChild_.html 450 | # 451 | def getChild(index) 452 | @children&.[](index) 453 | end 454 | 455 | # Returns the number of children. 456 | # 457 | # @return [Integer] child count 458 | # 459 | # @see https://processing.org/reference/PShape_getChildCount_.html 460 | # 461 | def getChildCount() 462 | @children&.size || 0 463 | end 464 | 465 | # Applies translation matrix to the shape. 466 | # 467 | # @overload translate(x, y) 468 | # @overload translate(x, y, z) 469 | # 470 | # @param x [Numeric] left/right translation 471 | # @param y [Numeric] up/down translation 472 | # @param z [Numeric] forward/backward translation 473 | # 474 | # @return [nil] nil 475 | # 476 | # @see https://processing.org/reference/PShape_translate_.html 477 | # 478 | def translate(x, y, z = 0) 479 | matrix__.translate! x, y, z 480 | nil 481 | end 482 | 483 | # Applies scale matrix to the shape. 484 | # 485 | # @overload scale(s) 486 | # @overload scale(x, y) 487 | # @overload scale(x, y, z) 488 | # 489 | # @param s [Numeric] horizontal and vertical scale 490 | # @param x [Numeric] horizontal scale 491 | # @param y [Numeric] vertical scale 492 | # @param z [Numeric] depth scale 493 | # 494 | # @return [nil] nil 495 | # 496 | # @see https://processing.org/reference/PShape_scale_.html 497 | # 498 | def scale(x, y = nil, z = 1) 499 | matrix__.scale! x, (y || x), z 500 | nil 501 | end 502 | 503 | # Applies rotation matrix to the shape. 504 | # 505 | # @param angle [Numeric] angle for rotation 506 | # 507 | # @return [nil] nil 508 | # 509 | # @see https://processing.org/reference/PShape_rotate_.html 510 | # 511 | def rotate(angle) 512 | matrix__.rotate! @context.toDegrees__(angle) 513 | nil 514 | end 515 | 516 | # Applies rotation around the x-axis to the shape. 517 | # 518 | # @param angle [Numeric] angle for rotation 519 | # 520 | # @return [nil] nil 521 | # 522 | # @see https://processing.org/reference/PShape_rotateX_.html 523 | # 524 | def rotateX(angle) 525 | matrix__.rotate! @context.toDegrees__(angle), 1, 0, 0 526 | nil 527 | end 528 | 529 | # Applies rotation around the y-axis to the shape. 530 | # 531 | # @param angle [Numeric] angle for rotation 532 | # 533 | # @return [nil] nil 534 | # 535 | # @see https://processing.org/reference/PShape_rotateY_.html 536 | # 537 | def rotateY(angle) 538 | matrix__.rotate! @context.toDegrees__(angle), 0, 1, 0 539 | nil 540 | end 541 | 542 | # Applies rotation around the z-axis to the shape. 543 | # 544 | # @param angle [Numeric] angle for rotation 545 | # 546 | # @return [nil] nil 547 | # 548 | # @see https://processing.org/reference/PShape_rotateZ_.html 549 | # 550 | def rotateZ(angle) 551 | matrix__.rotate! @context.toDegrees__(angle), 0, 0, 1 552 | nil 553 | end 554 | 555 | # Reset the transformation matrix. 556 | # 557 | # @return [nil] nil 558 | # 559 | # @see https://processing.org/reference/PShape_resetMatrix_.html 560 | # 561 | def resetMatrix() 562 | @matrix = nil 563 | nil 564 | end 565 | 566 | # @private 567 | def clearCache__() 568 | @polygon = nil# clear cache 569 | end 570 | 571 | # @private 572 | def matrix__() 573 | @matrix ||= Rays::Matrix.new 574 | end 575 | 576 | # @private 577 | def getInternal__() 578 | unless @polygon 579 | return nil unless @points && @close != nil 580 | @polygon = self.class.createPolygon__( 581 | @type, @points, @close, @colors, @texcoords) 582 | @polygon += @contours if @polygon 583 | end 584 | @polygon 585 | end 586 | 587 | # @private 588 | def draw__(painter, x, y, w = nil, h = nil) 589 | p, poly = painter, getInternal__ 590 | 591 | matrix_ = nil 592 | if @matrix && (poly || @children) 593 | matrix_ = p.matrix 594 | p.matrix = matrix_ * @matrix 595 | end 596 | 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 604 | if w || h 605 | p.polygon poly, x, y, w,h 606 | else 607 | p.polygon poly, x, y 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_ 614 | end 615 | 616 | @children&.each {|o| o.draw__ p, x, y, w, h} 617 | 618 | p.matrix = matrix_ if matrix_ 619 | end 620 | 621 | # @private 622 | def self.createPolygon__( 623 | type, points, close = false, colors = nil, texcoords = nil) 624 | 625 | kwargs = {colors: colors, texcoords: texcoords} 626 | g, p = GraphicsContext, Rays::Polygon 627 | case type 628 | when g::POINTS then p.points( *points) 629 | when g::LINES then p.lines( *points) 630 | when g::TRIANGLES then p.triangles( *points, **kwargs) 631 | when g::TRIANGLE_FAN then p.triangle_fan( *points, **kwargs) 632 | when g::TRIANGLE_STRIP then p.triangle_strip(*points, **kwargs) 633 | when g::QUADS then p.quads( *points, **kwargs) 634 | when g::QUAD_STRIP then p.quad_strip( *points, **kwargs) 635 | when g::TESS, nil then p.new( *points, **kwargs, loop: close) 636 | else raise ArgumentError, "invalid polygon type '#{type}'" 637 | end 638 | end 639 | 640 | end# Shape 641 | 642 | 643 | end# Processing 644 | -------------------------------------------------------------------------------- /lib/processing/svg.rb: -------------------------------------------------------------------------------- 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 249 | -------------------------------------------------------------------------------- /lib/processing/touch.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Touch object. 5 | # 6 | class Touch 7 | 8 | # Identifier of each touch 9 | # 10 | attr_reader :id 11 | 12 | # Horizontal position of touch 13 | # 14 | # @return [Numeric] position x 15 | # 16 | attr_reader :x 17 | 18 | # Vertical position of touch 19 | # 20 | # @return [Numeric] position y 21 | # 22 | attr_reader :y 23 | 24 | # @private 25 | def initialize(id, x, y) 26 | @id, @x, @y = id, x, y 27 | end 28 | 29 | # Returns a string containing a human-readable representation of object. 30 | # 31 | # @return [String] inspected text 32 | # 33 | def inspect() 34 | "#" 35 | end 36 | 37 | end# Touch 38 | 39 | 40 | end# Processing 41 | -------------------------------------------------------------------------------- /lib/processing/vector.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # Vector class. 5 | # 6 | # @see https://processing.org/reference/PVector.html 7 | # @see https://p5js.org/reference/p5/p5.Vector/ 8 | # 9 | class Vector 10 | 11 | include Comparable 12 | 13 | # Initialize vector object. 14 | # 15 | # @overload new() 16 | # @overload new(x) 17 | # @overload new(x, y) 18 | # @overload new(x, y, z) 19 | # @overload new(v) 20 | # @overload new(a) 21 | # 22 | # @param x [Numeric] x of vector 23 | # @param y [Numeric] y of vector 24 | # @param z [Numeric] z of vector 25 | # @param v [Vector] vector object to copy 26 | # @param a [Array] array like [x, y, z] 27 | # 28 | # @see https://processing.org/reference/PVector.html 29 | # @see https://p5js.org/reference/p5/p5.Vector/ 30 | # 31 | def initialize(x = 0, y = 0, z = 0, context: nil) 32 | @point = case x 33 | when Rays::Point then x.dup 34 | when Vector then x.getInternal__.dup 35 | when Array then Rays::Point.new x[0] || 0, x[1] || 0, x[2] || 0 36 | else Rays::Point.new x || 0, y || 0, z || 0 37 | end 38 | @context = context || Context.context__ 39 | end 40 | 41 | # Initializer for dup or clone 42 | # 43 | def initialize_copy(o) 44 | @point = o.getInternal__.dup 45 | end 46 | 47 | # Copy vector object 48 | # 49 | # @return [Vector] duplicated vector object 50 | # 51 | # @see https://processing.org/reference/PVector_copy_.html 52 | # @see https://p5js.org/reference/p5.Vector/copy/ 53 | # 54 | alias copy dup 55 | 56 | # Sets x, y and z. 57 | # 58 | # @overload set(x) 59 | # @overload set(x, y) 60 | # @overload set(x, y, z) 61 | # @overload set(v) 62 | # @overload set(a) 63 | # 64 | # @param x [Numeric] x of vector 65 | # @param y [Numeric] y of vector 66 | # @param z [Numeric] z of vector 67 | # @param v [Vector] vector object to copy 68 | # @param a [Array] array with x, y, z 69 | # 70 | # @return [nil] nil 71 | # 72 | # @see https://processing.org/reference/PVector_set_.html 73 | # @see https://p5js.org/reference/p5.Vector/set/ 74 | # 75 | def set(*args) 76 | initialize(*args) 77 | self 78 | end 79 | 80 | # Gets x value. 81 | # 82 | # @return [Numeric] x value of vector 83 | # 84 | # @see https://processing.org/reference/PVector_x.html 85 | # @see https://p5js.org/reference/p5.Vector/x/ 86 | # 87 | def x() 88 | @point.x 89 | end 90 | 91 | # Gets y value. 92 | # 93 | # @return [Numeric] y value of vector 94 | # 95 | # @see https://processing.org/reference/PVector_y.html 96 | # @see https://p5js.org/reference/p5.Vector/y/ 97 | # 98 | def y() 99 | @point.y 100 | end 101 | 102 | # Gets z value. 103 | # 104 | # @return [Numeric] z value of vector 105 | # 106 | # @see https://processing.org/reference/PVector_z.html 107 | # @see https://p5js.org/reference/p5.Vector/z/ 108 | # 109 | def z() 110 | @point.z 111 | end 112 | 113 | # Sets x value. 114 | # 115 | # @return [Numeric] x value of vector 116 | # 117 | # @see https://processing.org/reference/PVector_x.html 118 | # @see https://p5js.org/reference/p5.Vector/x/ 119 | # 120 | def x=(x) 121 | @point.x = x 122 | end 123 | 124 | # Sets y value. 125 | # 126 | # @return [Numeric] y value of vector 127 | # 128 | # @see https://processing.org/reference/PVector_y.html 129 | # @see https://p5js.org/reference/p5.Vector/y/ 130 | # 131 | def y=(y) 132 | @point.y = y 133 | end 134 | 135 | # Sets z value. 136 | # 137 | # @return [Numeric] z value of vector 138 | # 139 | # @see https://processing.org/reference/PVector_z.html 140 | # @see https://p5js.org/reference/p5.Vector/z/ 141 | # 142 | def z=(z) 143 | @point.z = z 144 | end 145 | 146 | # Returns the interpolated vector between 2 vectors. 147 | # 148 | # @overload lerp(v, amount) 149 | # @overload lerp(x, y, amount) 150 | # @overload lerp(x, y, z, amount) 151 | # 152 | # @param v [Vector] vector to interpolate 153 | # @param x [Numeric] x of vector to interpolate 154 | # @param y [Numeric] y of vector to interpolate 155 | # @param z [Numeric] z of vector to interpolate 156 | # @param amount [Numeric] amount to interpolate 157 | # 158 | # @return [Vector] interporated vector 159 | # 160 | # @see https://processing.org/reference/PVector_lerp_.html 161 | # @see https://p5js.org/reference/p5.Vector/lerp/ 162 | # 163 | def lerp(*args, amount) 164 | v = toVector__(*args) 165 | self.x = x + (v.x - x) * amount 166 | self.y = y + (v.y - y) * amount 167 | self.z = z + (v.z - z) * amount 168 | self 169 | end 170 | 171 | # Returns the interpolated vector between 2 vectors. 172 | # 173 | # @param v1 [Vector] vector to interpolate 174 | # @param v2 [Vector] vector to interpolate 175 | # @param amount [Numeric] amount to interpolate 176 | # 177 | # @return [Vector] interporated vector 178 | # 179 | # @see https://processing.org/reference/PVector_lerp_.html 180 | # @see https://p5js.org/reference/p5.Vector/lerp/ 181 | # 182 | def self.lerp(v1, v2, amount) 183 | v1.dup.lerp v2, amount 184 | end 185 | 186 | # Returns x, y, z as an array 187 | # 188 | # @param n [Numeric] number of dimensions 189 | # 190 | # @return [Array] array of x, y, z 191 | # 192 | # @see https://processing.org/reference/PVector_array_.html 193 | # @see https://p5js.org/reference/p5.Vector/array/ 194 | # 195 | def array(n = 3) 196 | @point.to_a n 197 | end 198 | 199 | alias to_a array 200 | 201 | # Adds a vector. 202 | # 203 | # @overload add(v) 204 | # @overload add(x, y) 205 | # @overload add(x, y, z) 206 | # 207 | # @param v [Vector] vector to add 208 | # @param x [Vector] x of vector to add 209 | # @param y [Vector] y of vector to add 210 | # @param z [Vector] z of vector to add 211 | # 212 | # @return [Vector] added vector 213 | # 214 | # @see https://processing.org/reference/PVector_add_.html 215 | # @see https://p5js.org/reference/p5.Vector/add/ 216 | # 217 | def add(*args) 218 | @point += toVector__(*args).getInternal__ 219 | self 220 | end 221 | 222 | # Subtracts a vector. 223 | # 224 | # @overload sub(v) 225 | # @overload sub(x, y) 226 | # @overload sub(x, y, z) 227 | # 228 | # @param v [Vector] vector to subtract 229 | # @param x [Vector] x of vector to subtract 230 | # @param y [Vector] y of vector to subtract 231 | # @param z [Vector] z of vector to subtract 232 | # 233 | # @return [Vector] subtracted vector 234 | # 235 | # @see https://processing.org/reference/PVector_sub_.html 236 | # @see https://p5js.org/reference/p5.Vector/sub/ 237 | # 238 | def sub(*args) 239 | @point -= toVector__(*args).getInternal__ 240 | self 241 | end 242 | 243 | # Multiplies a vector by scalar. 244 | # 245 | # @param num [Numeric] number to multiply the vector 246 | # 247 | # @return [Vector] multiplied vector 248 | # 249 | # @see https://processing.org/reference/PVector_mult_.html 250 | # @see https://p5js.org/reference/p5.Vector/mult/ 251 | # 252 | def mult(num) 253 | @point *= num 254 | self 255 | end 256 | 257 | # Divides a vector by scalar. 258 | # 259 | # @param num [Numeric] number to divide the vector 260 | # 261 | # @return [Vector] divided vector 262 | # 263 | # @see https://processing.org/reference/PVector_div_.html 264 | # @see https://p5js.org/reference/p5.Vector/div/ 265 | # 266 | def div(num) 267 | @point /= num 268 | self 269 | end 270 | 271 | # Adds a vector. 272 | # 273 | # @param v [Vector] vector to add 274 | # 275 | # @return [Vector] added vector 276 | # 277 | # @see https://processing.org/reference/PVector_add_.html 278 | # @see https://p5js.org/reference/p5.Vector/add/ 279 | # 280 | def +(v) 281 | dup.add v 282 | end 283 | 284 | # Subtracts a vector. 285 | # 286 | # @param v [Vector] vector to subtract 287 | # 288 | # @return [Vector] subtracted vector 289 | # 290 | # @see https://processing.org/reference/PVector_sub_.html 291 | # @see https://p5js.org/reference/p5.Vector/sub/ 292 | # 293 | def -(v) 294 | dup.sub v 295 | end 296 | 297 | # Multiplies a vector by scalar. 298 | # 299 | # @param num [Numeric] number to multiply the vector 300 | # 301 | # @return [Vector] multiplied vector 302 | # 303 | # @see https://processing.org/reference/PVector_mult_.html 304 | # @see https://p5js.org/reference/p5.Vector/mult/ 305 | # 306 | def *(num) 307 | dup.mult num 308 | end 309 | 310 | # Divides a vector by scalar. 311 | # 312 | # @param num [Numeric] number to divide the vector 313 | # 314 | # @return [Vector] divided vector 315 | # 316 | # @see https://processing.org/reference/PVector_div_.html 317 | # @see https://p5js.org/reference/p5.Vector/div/ 318 | # 319 | def /(num) 320 | dup.div num 321 | end 322 | 323 | # Negate a vector. 324 | # 325 | # @return [Vector] negated vector 326 | # 327 | def -@() 328 | dup.mult -1 329 | end 330 | 331 | # Adds 2 vectors. 332 | # 333 | # @overload add(v1, v2) 334 | # @overload add(v1, v2, target) 335 | # 336 | # @param v1 [Vector] a vector 337 | # @param v2 [Vector] another vector 338 | # @param target [Vector] vector to store added vector 339 | # 340 | # @return [Vector] added vector 341 | # 342 | # @see https://processing.org/reference/PVector_add_.html 343 | # @see https://p5js.org/reference/p5.Vector/add/ 344 | # 345 | def self.add(v1, v2, target = nil) 346 | v = v1 + v2 347 | target.set v if self === target 348 | v 349 | end 350 | 351 | # Subtracts 2 vectors. 352 | # 353 | # @overload sub(v1, v2) 354 | # @overload sub(v1, v2, target) 355 | # 356 | # @param v1 [Vector] a vector 357 | # @param v2 [Vector] another vector 358 | # @param target [Vector] vector to store subtracted vector 359 | # 360 | # @return [Vector] subtracted vector 361 | # 362 | # @see https://processing.org/reference/PVector_sub_.html 363 | # @see https://p5js.org/reference/p5.Vector/sub/ 364 | # 365 | def self.sub(v1, v2, target = nil) 366 | v = v1 - v2 367 | target.set v if self === target 368 | v 369 | end 370 | 371 | # Multiplies a vector by scalar. 372 | # 373 | # @overload mult(v, num) 374 | # @overload mult(v, num, target) 375 | # 376 | # @param v [Vector] a vector 377 | # @param num [Numeric] number to multiply the vector 378 | # @param target [Vector] vector to store multiplied vector 379 | # 380 | # @return [Vector] multiplied vector 381 | # 382 | # @see https://processing.org/reference/PVector_mult_.html 383 | # @see https://p5js.org/reference/p5.Vector/mult/ 384 | # 385 | def self.mult(v1, num, target = nil) 386 | v = v1 * num 387 | target.set v if self === target 388 | v 389 | end 390 | 391 | # Divides a vector by scalar. 392 | # 393 | # @overload div(v, num) 394 | # @overload div(v, num, target) 395 | # 396 | # @param v [Vector] a vector 397 | # @param num [Numeric] number to divide the vector 398 | # @param target [Vector] vector to store divided vector 399 | # 400 | # @return [Vector] divided vector 401 | # 402 | # @see https://processing.org/reference/PVector_div_.html 403 | # @see https://p5js.org/reference/p5.Vector/div/ 404 | # 405 | def self.div(v1, num, target = nil) 406 | v = v1 / num 407 | target.set v if self === target 408 | v 409 | end 410 | 411 | # Returns the length of the vector. 412 | # 413 | # @return [Numeric] length 414 | # 415 | # @see https://processing.org/reference/PVector_mag_.html 416 | # @see https://p5js.org/reference/p5.Vector/mag/ 417 | # 418 | def mag() 419 | @point.length 420 | end 421 | 422 | # Returns squared length of the vector. 423 | # 424 | # @return [Numeric] squared length 425 | # 426 | # @see https://processing.org/reference/PVector_magSq_.html 427 | # @see https://p5js.org/reference/p5.Vector/magSq/ 428 | # 429 | def magSq() 430 | Rays::Point::dot(@point, @point) 431 | end 432 | 433 | # Changes the length of the vector. 434 | # 435 | # @overload setMag(len) 436 | # @overload setMag(target, len) 437 | # 438 | # @param len [Numeric] length of new vector 439 | # @param target [Vector] vector to store new vector 440 | # 441 | # @return [Vector] vector with new length 442 | # 443 | # @see https://processing.org/reference/PVector_setMag_.html 444 | # @see https://p5js.org/reference/p5.Vector/setMag/ 445 | # 446 | def setMag(target = nil, len) 447 | (target || self).set @point.normal * len 448 | end 449 | 450 | # Changes the length of the vector to 1.0. 451 | # 452 | # @param target [Vector] vector to store the normalized vector 453 | # 454 | # @return [Vector] normalized vector 455 | # 456 | # @see https://processing.org/reference/PVector_normalize_.html 457 | # @see https://p5js.org/reference/p5.Vector/normalize/ 458 | # 459 | def normalize(target = nil) 460 | (target || self).set @point.normal 461 | end 462 | 463 | # Changes the length of the vector if it's length is greater than the max value. 464 | # 465 | # @param max [Numeric] max length 466 | # 467 | # @return [Vector] new vector 468 | # 469 | # @see https://processing.org/reference/PVector_limit_.html 470 | # @see https://p5js.org/reference/p5.Vector/limit/ 471 | # 472 | def limit(max) 473 | setMag max if magSq > max ** 2 474 | self 475 | end 476 | 477 | # Returns the distance of 2 vectors. 478 | # 479 | # @param v [Vector] a vector 480 | # 481 | # @return [Numeric] the distance 482 | # 483 | # @see https://processing.org/reference/PVector_dist_.html 484 | # @see https://p5js.org/reference/p5.Vector/dist/ 485 | # 486 | def dist(v) 487 | (self - v).mag 488 | end 489 | 490 | # Returns the distance of 2 vectors. 491 | # 492 | # @param v1 [Vector] a vector 493 | # @param v2 [Vector] another vector 494 | # 495 | # @return [Numeric] the distance 496 | # 497 | # @see https://processing.org/reference/PVector_dist_.html 498 | # @see https://p5js.org/reference/p5.Vector/dist/ 499 | # 500 | def self.dist(v1, v2) 501 | v1.dist v2 502 | end 503 | 504 | # Calculates the dot product of 2 vectors. 505 | # 506 | # @overload dot(v) 507 | # @overload dot(x, y) 508 | # @overload dot(x, y, z) 509 | # 510 | # @param v [Vector] a vector 511 | # @param x [Numeric] x of vector 512 | # @param y [Numeric] y of vector 513 | # @param z [Numeric] z of vector 514 | # 515 | # @return [Numeric] result of dot product 516 | # 517 | # @see https://processing.org/reference/PVector_dot_.html 518 | # @see https://p5js.org/reference/p5.Vector/dot/ 519 | # 520 | def dot(*args) 521 | Rays::Point::dot getInternal__, toVector__(*args).getInternal__ 522 | end 523 | 524 | # Calculates the dot product of 2 vectors. 525 | # 526 | # @param v1 [Vector] a vector 527 | # @param v2 [Vector] another vector 528 | # 529 | # @return [Numeric] result of dot product 530 | # 531 | # @see https://processing.org/reference/PVector_dot_.html 532 | # @see https://p5js.org/reference/p5.Vector/dot/ 533 | # 534 | def self.dot(v1, v2) 535 | v1.dot v2 536 | end 537 | 538 | # Calculates the cross product of 2 vectors. 539 | # 540 | # @overload cross(v) 541 | # @overload cross(x, y) 542 | # @overload cross(x, y, z) 543 | # 544 | # @param v [Vector] a vector 545 | # @param x [Numeric] x of vector 546 | # @param y [Numeric] y of vector 547 | # @param z [Numeric] z of vector 548 | # 549 | # @return [Numeric] result of cross product 550 | # 551 | # @see https://processing.org/reference/PVector_cross_.html 552 | # @see https://p5js.org/reference/p5.Vector/cross/ 553 | # 554 | def cross(a, *rest) 555 | target = self.class === rest.last ? rest.pop : nil 556 | v = self.class.new Rays::Point::cross getInternal__, toVector__(a, *rest).getInternal__ 557 | target.set v if self.class === target 558 | v 559 | end 560 | 561 | # Calculates the cross product of 2 vectors. 562 | # 563 | # @param v1 [Vector] a vector 564 | # @param v2 [Vector] another vector 565 | # 566 | # @return [Numeric] result of cross product 567 | # 568 | # @see https://processing.org/reference/PVector_cross_.html 569 | # @see https://p5js.org/reference/p5.Vector/cross/ 570 | # 571 | def self.cross(v1, v2, target = nil) 572 | v1.cross v2, target 573 | end 574 | 575 | # Rotate the vector. 576 | # 577 | # @param angle [Numeric] the angle of rotation 578 | # 579 | # @return [Vector] rotated this object 580 | # 581 | # @see https://processing.org/reference/PVector_rotate_.html 582 | # @see https://p5js.org/reference/p5.Vector/rotate/ 583 | # 584 | def rotate(angle) 585 | deg = @context ? 586 | @context.toDegrees__(angle) : angle * GraphicsContext::RAD2DEG__ 587 | @point.rotate! deg 588 | self 589 | end 590 | 591 | # Returns the angle of rotation for this vector. 592 | # 593 | # @return [Numeric] the angle in radians 594 | # 595 | # @see https://processing.org/reference/PVector_heading_.html 596 | # @see https://p5js.org/reference/p5.Vector/heading/ 597 | # 598 | def heading() 599 | Math.atan2 y, x 600 | end 601 | 602 | # Returns rotated new vector. 603 | # 604 | # @param angle [Numeric] the angle of rotation 605 | # @param target [Vector] vector to store new vector 606 | # 607 | # @return [Vector] rotated vector 608 | # 609 | # @see https://processing.org/reference/PVector_fromAngle_.html 610 | # @see https://p5js.org/reference/p5.Vector/fromAngle/ 611 | # 612 | def self.fromAngle(angle, target = nil) 613 | v = self.new(1, 0, 0).rotate(angle) 614 | target.set v if target 615 | v 616 | end 617 | 618 | # Returns angle between 2 vectors. 619 | # 620 | # @param v1 [Vector] a vector 621 | # @param v2 [Vector] another vector 622 | # 623 | # @return [Numeric] angle in radians 624 | # 625 | # @see https://processing.org/reference/PVector_angleBetween_.html 626 | # @see https://p5js.org/reference/p5.Vector/angleBetween/ 627 | # 628 | def self.angleBetween(v1, v2) 629 | x1, y1, z1 = v1.array 630 | x2, y2, z2 = v2.array 631 | return 0 if (x1 == 0 && y1 == 0 && z1 == 0) || (x2 == 0 && y2 == 0 && z2 == 0) 632 | 633 | x = dot(v1, v2) / (v1.mag * v2.mag) 634 | return Math::PI if x <= -1 635 | return 0 if x >= 1 636 | return Math.acos x 637 | end 638 | 639 | # Returns a new 2D unit vector with a random direction. 640 | # 641 | # @param target [Vector] a vector to store the new vector 642 | # 643 | # @return [Vector] a random vector 644 | # 645 | # @see https://processing.org/reference/PVector_random2D_.html 646 | # @see https://p5js.org/reference/p5.Vector/random2D/ 647 | # 648 | def self.random2D(target = nil) 649 | v = self.new(1, 0, 0) 650 | v.getInternal__.rotate! rand 0.0...360.0 651 | target.set v if target 652 | v 653 | end 654 | 655 | # Returns a new 3D unit vector with a random direction. 656 | # 657 | # @param target [Vector] a vector to store the new vector 658 | # 659 | # @return [Vector] a random vector 660 | # 661 | # @see https://processing.org/reference/PVector_random3D_.html 662 | # @see https://p5js.org/reference/p5.Vector/random3D/ 663 | # 664 | def self.random3D(target = nil) 665 | angle = rand 0.0...(Math::PI * 2) 666 | z = rand(-1.0..1.0) 667 | z2 = z ** 2 668 | x = Math.sqrt(1.0 - z2) * Math.cos(angle) 669 | y = Math.sqrt(1.0 - z2) * Math.sin(angle) 670 | v = self.new x, y, z 671 | target.set v if target 672 | v 673 | end 674 | 675 | # Returns a string containing a human-readable representation of object. 676 | # 677 | # @return [String] inspected text 678 | # 679 | def inspect() 680 | "#<#{self.class.name}: #{x}, #{y}, #{z}>" 681 | end 682 | 683 | # @private 684 | def <=>(o) 685 | @point <=> o&.getInternal__ 686 | end 687 | 688 | # @private 689 | def getInternal__() 690 | @point 691 | end 692 | 693 | # @private 694 | private def toVector__(*args) 695 | self.class === args.first ? args.first : self.class.new(*args) 696 | end 697 | 698 | end# Vector 699 | 700 | 701 | end# Processing 702 | -------------------------------------------------------------------------------- /lib/processing/window.rb: -------------------------------------------------------------------------------- 1 | module Processing 2 | 3 | 4 | # @private 5 | class Window < Reflex::Window 6 | 7 | include Xot::Inspectable 8 | 9 | def initialize(width = 500, height = 500, *args, **kwargs, &block) 10 | Processing.instance_variable_set :@window, self 11 | 12 | @events = [] 13 | @active = false 14 | @error = nil 15 | @auto_resize = true 16 | @canvas = Canvas.new self, width, height 17 | @canvas_view = add CanvasView.new name: :canvas 18 | @overlay_view = @canvas_view.add Reflex::View.new name: :overlay 19 | 20 | super(*args, size: [width, height], **kwargs, &block) 21 | self.center = screen.center 22 | end 23 | 24 | attr_accessor :setup, :update, :draw, :move, :resize, :motion, 25 | :key_down, :key_up, :pointer_down, :pointer_up, :pointer_move, :wheel, 26 | :note_on, :note_off, :control_change, 27 | :before_draw, :after_draw, :update_window, :update_canvas 28 | 29 | attr_accessor :auto_resize 30 | 31 | attr_reader :canvas 32 | 33 | def event() 34 | @events.last 35 | end 36 | 37 | def active?() 38 | @active 39 | end 40 | 41 | def add_overlay(view) 42 | @overlay_view.add view 43 | end 44 | 45 | def remove_overlay(view) 46 | @overlay_view.remove view 47 | end 48 | 49 | def start(&block) 50 | draw_canvas do 51 | block.call if block 52 | on_setup 53 | end 54 | end 55 | 56 | def on_setup() 57 | call_block @setup, nil 58 | end 59 | 60 | def on_change_pixel_density(pixel_density) 61 | resize_canvas( 62 | @canvas.width, @canvas.height, 63 | window_pixel_density: pixel_density) 64 | end 65 | 66 | def on_activate(e) 67 | @active = true 68 | end 69 | 70 | def on_deactivate(e) 71 | @active = false 72 | end 73 | 74 | def on_update(e) 75 | draw_canvas {call_block @update_window, e} if @update_window 76 | end 77 | 78 | def on_draw(e) 79 | painter.pixel_density.tap do |pd| 80 | prev, @prev_pixel_density = @prev_pixel_density, pd 81 | on_change_pixel_density pd if prev && pd != prev 82 | end 83 | update_canvas_view 84 | end 85 | 86 | def on_key_down(e) 87 | draw_canvas {call_block @key_down, e} if @key_down 88 | end 89 | 90 | def on_key_up(e) 91 | draw_canvas {call_block @key_up, e} if @key_up 92 | end 93 | 94 | def on_note_on(e) 95 | draw_canvas {call_block @note_on, e} if @note_on 96 | end 97 | 98 | def on_note_off(e) 99 | draw_canvas {call_block @note_off, e} if @note_off 100 | end 101 | 102 | def on_control_change(e) 103 | draw_canvas {call_block @control_change, e} if @control_change 104 | end 105 | 106 | def on_move(e) 107 | draw_canvas {call_block @move, e} if @move 108 | end 109 | 110 | def on_resize(e) 111 | resize_canvas e.width, e.height if @auto_resize 112 | draw_canvas {call_block @resize, e} if @resize 113 | end 114 | 115 | def on_motion(e) 116 | draw_canvas {call_block @motion, e} if @motion 117 | end 118 | 119 | def on_canvas_update(e) 120 | call_block @update, e 121 | @canvas_view.redraw 122 | end 123 | 124 | def on_canvas_draw(e) 125 | draw_canvas {call_block @draw, e} if @draw 126 | draw_screen e.painter 127 | end 128 | 129 | def on_canvas_pointer(e) 130 | block = case e.action 131 | when :down then @pointer_down 132 | when :up, :cancel then @pointer_up 133 | when :move then @pointer_move 134 | end 135 | draw_canvas {call_block block, e} if block 136 | end 137 | 138 | def on_canvas_wheel(e) 139 | draw_canvas {call_block @wheel, e} if @wheel 140 | end 141 | 142 | def on_canvas_resize(e) 143 | resize_canvas e.width, e.height if @auto_resize 144 | draw_canvas {call_block @resize, e} if @resize 145 | end 146 | 147 | def resize_canvas( 148 | width, height, 149 | pixel_density = nil, 150 | window_pixel_density: nil, 151 | antialiasing: nil) 152 | 153 | painting = @canvas.painter.painting? 154 | @canvas.painter.__send__ :end_paint if painting 155 | 156 | @pixel_density = pixel_density if pixel_density 157 | 158 | resized = 159 | begin 160 | pd = @pixel_density || window_pixel_density 161 | @canvas.resize width, height, pd, antialiasing 162 | ensure 163 | @canvas.painter.__send__ :begin_paint if painting 164 | end 165 | 166 | @update_canvas&.call @canvas.image, @canvas.painter if resized 167 | end 168 | 169 | private 170 | 171 | def update_canvas_view() 172 | scrollx, scrolly, zoom = get_scroll_and_zoom 173 | @canvas_view.scroll_to scrollx, scrolly 174 | @canvas_view.zoom = zoom 175 | @overlay_view.size = @canvas.image.size 176 | end 177 | 178 | def get_scroll_and_zoom() 179 | ww, wh = width.to_f, height.to_f 180 | cw, ch = @canvas.image.width.to_f, @canvas.image.height.to_f 181 | return 0, 0, 1 if ww == 0 || wh == 0 || cw == 0 || ch == 0 182 | 183 | wratio, cratio = ww / wh, cw / ch 184 | if wratio == cratio 185 | return 0, 0, ww / cw 186 | elsif wratio > cratio 187 | w = wh * cratio 188 | return (ww - w) / 2, 0, w / cw 189 | else 190 | h = ww / cratio 191 | return 0, (wh - h) / 2, ww / cw 192 | end 193 | end 194 | 195 | def draw_canvas(&block) 196 | drawing = self.drawing? 197 | begin_draw unless drawing 198 | block.call 199 | ensure 200 | end_draw unless drawing 201 | end 202 | 203 | def begin_draw() 204 | @canvas.painter.__send__ :begin_paint 205 | @before_draw&.call 206 | end 207 | 208 | def end_draw() 209 | @after_draw&.call 210 | @canvas.painter.__send__ :end_paint 211 | end 212 | 213 | def drawing?() 214 | @canvas.painter.painting? 215 | end 216 | 217 | def draw_screen(painter) 218 | painter.image @canvas.render 219 | end 220 | 221 | def call_block(block, event, *args) 222 | @events.push event 223 | block.call event, *args if block && !@error 224 | rescue Exception => e 225 | @error = e 226 | $stderr.puts e.full_message 227 | ensure 228 | @events.pop 229 | end 230 | 231 | end# Window 232 | 233 | 234 | # @private 235 | class Window::Canvas 236 | 237 | def initialize(window, width, height) 238 | @framebuffer = nil 239 | @paintable = nil 240 | @painter = window.painter 241 | 242 | @painter.miter_limit = 10 243 | 244 | resize width, height 245 | end 246 | 247 | attr_reader :painter 248 | 249 | def resize(width, height, pixel_density = nil, antialiasing = nil) 250 | return false if width <= 0 || height <= 0 251 | 252 | cs = @framebuffer&.color_space || Rays::RGBA 253 | pd = pixel_density || (@framebuffer || @painter).pixel_density 254 | aa = antialiasing == nil ? antialiasing? : (antialiasing && pd < 2) 255 | return false if 256 | width == @framebuffer&.width && 257 | height == @framebuffer&.height && 258 | pd == @framebuffer&.pixel_density && 259 | aa == antialiasing? 260 | 261 | old_paintable, old_painter = @paintable, @painter 262 | 263 | @framebuffer = Rays::Image.new width, height, cs, pixel_density: pd 264 | @paintable = aa ? 265 | Rays::Image.new(width, height, cs, pixel_density: pd * 2) : @framebuffer 266 | @painter = @paintable.painter 267 | 268 | @painter.paint {image old_paintable} if old_paintable 269 | copy_painter old_painter, @painter 270 | 271 | GC.start 272 | return true 273 | end 274 | 275 | def render() 276 | @framebuffer.paint {|p| p.image @paintable} if antialiasing? 277 | @framebuffer 278 | end 279 | 280 | def image() 281 | @paintable 282 | end 283 | 284 | def width() 285 | @framebuffer.width 286 | end 287 | 288 | def height() 289 | @framebuffer.height 290 | end 291 | 292 | def pixel_density() 293 | @framebuffer.pixel_density 294 | end 295 | 296 | def antialiasing?() 297 | !!@framebuffer && !!@paintable && @framebuffer != @paintable 298 | end 299 | 300 | private 301 | 302 | def copy_painter(from, to) 303 | to.fill = from.fill 304 | to.stroke = from.stroke 305 | to.stroke_width = from.stroke_width 306 | to.stroke_cap = from.stroke_cap 307 | to.stroke_join = from.stroke_join 308 | to.miter_limit = from.miter_limit 309 | to.clip = from.clip 310 | to.blend_mode = from.blend_mode 311 | to.font = from.font 312 | to.texture = from.texture 313 | to.texcoord_mode = from.texcoord_mode 314 | to.texcoord_wrap = from.texcoord_wrap 315 | to.shader = from.shader 316 | end 317 | 318 | end# Window::Canvas 319 | 320 | 321 | # @private 322 | class Window::CanvasView < Reflex::View 323 | 324 | def on_update(e) 325 | window.on_canvas_update e 326 | Thread.pass 327 | end 328 | 329 | def on_draw(e) 330 | window.on_canvas_draw e 331 | end 332 | 333 | def on_pointer(e) 334 | window.on_canvas_pointer e 335 | end 336 | 337 | def on_wheel(e) 338 | window.on_canvas_wheel e 339 | end 340 | 341 | def on_resize(e) 342 | window.on_canvas_resize e 343 | end 344 | 345 | end# Window::CanvasView 346 | 347 | 348 | end# Processing 349 | -------------------------------------------------------------------------------- /processing.gemspec: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | 3 | 4 | require_relative 'lib/processing/extension' 5 | 6 | 7 | Gem::Specification.new do |s| 8 | glob = -> *patterns do 9 | patterns.map {|pat| Dir.glob(pat).to_a}.flatten 10 | end 11 | 12 | ext = Processing::Extension 13 | name = ext.name.downcase 14 | rdocs = glob.call *%w[README] 15 | 16 | s.name = name 17 | s.version = ext.version 18 | s.license = 'MIT' 19 | s.summary = 'Processing compatible Creative Coding Framework.' 20 | s.description = 'Creative Coding Framework has API compatible to Processing or p5.js.' 21 | s.authors = %w[xordog] 22 | s.email = 'xordog@gmail.com' 23 | s.homepage = "https://github.com/xord/processing" 24 | 25 | s.platform = Gem::Platform::RUBY 26 | s.required_ruby_version = '>= 3.0.0' 27 | 28 | s.add_dependency 'rexml' 29 | s.add_dependency 'xot', '~> 0.3.8', '>= 0.3.8' 30 | s.add_dependency 'rucy', '~> 0.3.8', '>= 0.3.8' 31 | s.add_dependency 'rays', '~> 0.3.8', '>= 0.3.8' 32 | s.add_dependency 'reflexion', '~> 0.3.9', '>= 0.3.9' 33 | 34 | s.files = `git ls-files`.split $/ 35 | s.test_files = s.files.grep %r{^(test|spec|features)/} 36 | s.extra_rdoc_files = rdocs.to_a 37 | end 38 | -------------------------------------------------------------------------------- /test/browser.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'ferrum' 3 | 4 | RUBY_URL = 'https://cdn.jsdelivr.net/npm/ruby-3_2-wasm-wasi@next/dist/browser.script.iife.js' 5 | P5JS_URL = 'https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js' 6 | P5RB_URL = 'https://raw.githubusercontent.com/ongaeshi/p5rb/421ce24c4a29c5787d143f8132eb610b73f60b92/docs/lib/p5.rb' 7 | 8 | P5RB_SRC = URI.open(P5RB_URL) {|f| f.read} 9 | 10 | def browser(width, height, headless: true) 11 | hash = ($browsers ||= {}) 12 | key = [width, height, headless] 13 | hash[key] ||= Ferrum::Browser.new headless: headless, window_size: [width, height + 200] 14 | end 15 | 16 | def get_svg_html(width, height, svg_xml) 17 | <<~END 18 | 19 | 20 | 23 | 30 | 31 | 32 | 33 | #{svg_xml} 34 | 35 | 36 | 37 | END 38 | end 39 | 40 | def get_p5rb_html(width, height, draw_src, webgl: false) 41 | <<~END 42 | 43 | 44 | 45 | 46 | 47 | 50 | 59 | 70 | 71 |
72 | 73 | END 74 | end 75 | 76 | def sleep_until(try: 3, timeout: 10, &block) 77 | now = -> offset = 0 {Time.now.to_f + offset} 78 | limit = now[timeout] 79 | until block.call 80 | if now[] > limit 81 | limit = now[timeout] 82 | try -= 1 83 | next if try > 0 84 | raise 'Drawing timed out in browser' 85 | end 86 | sleep 0.1 87 | end 88 | end 89 | 90 | def draw_on_browser(width, height, path, html, headless: true) 91 | b = browser width, height, headless: headless 92 | unless File.exist? path 93 | b.reset 94 | b.main_frame.content = html 95 | sleep_until do 96 | b.evaluate 'document.querySelector("#completed") != null' 97 | end 98 | b.screenshot path: path, area: {x: 0, y: 0, width: width, height: height} 99 | end 100 | sleep 1 101 | b.device_pixel_ratio 102 | end 103 | 104 | def draw_svg(width, height, svg_xml, path, headless: true) 105 | html = get_svg_html width, height, svg_xml 106 | draw_on_browser width, height, path, html, headless: headless 107 | end 108 | 109 | def draw_p5rb(width, height, draw_src, path, headless: true, webgl: false) 110 | html = get_p5rb_html width, height, draw_src, webgl: webgl 111 | draw_on_browser width, height, path, html, headless: headless 112 | end 113 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | %w[../xot ../rucy ../rays ../reflex .] 2 | .map {|s| File.expand_path "../#{s}/lib", __dir__} 3 | .each {|s| $:.unshift s if !$:.include?(s) && File.directory?(s)} 4 | 5 | require 'xot/test' 6 | require 'processing/all' 7 | 8 | require 'digest/md5' 9 | require 'fileutils' 10 | require 'tempfile' 11 | require 'test/unit' 12 | 13 | include Xot::Test 14 | 15 | 16 | DEFAULT_DRAW_HEADER = <<~END 17 | background 100 18 | fill 255, 0, 0 19 | stroke 0, 255, 0 20 | strokeWeight 50 21 | END 22 | 23 | THRESHOLD_TO_BE_FIXED = 0.0 24 | 25 | 26 | def test_with_browser?() 27 | (ENV['TEST_WITH_BROWSER'] || '0') != '0' 28 | end 29 | 30 | def md5(s) 31 | Digest::MD5.hexdigest s 32 | end 33 | 34 | def mkdir(dir: nil, filename: nil) 35 | path = dir || File.dirname(filename) 36 | FileUtils.mkdir_p path unless File.exist? path 37 | end 38 | 39 | def test_label(frame_offset = 1, suffix: nil) 40 | suffix = suffix ? "_#{suffix}" : '' 41 | caller_locations[frame_offset] 42 | .then {|loc| "#{loc.label}_#{loc.lineno}#{suffix}"} 43 | end 44 | 45 | def temp_path(ext: nil, &block) 46 | f = Tempfile.new 47 | path = f.path 48 | path += ext if ext 49 | f.close! 50 | block.call path 51 | File.delete path 52 | end 53 | 54 | def draw_output_path(label, *sources, ext: '.png', dir: ext) 55 | src = sources.compact.then {|ary| ary.empty? ? '' : "_#{md5 ary.join("\n")}"} 56 | path = File.join __dir__, dir, label + src + ext 57 | mkdir filename: path 58 | path 59 | end 60 | 61 | def get_pixels(image) 62 | %i[@image @image__] 63 | .map {image.instance_variable_get _1} 64 | .compact 65 | .first 66 | .pixels 67 | end 68 | 69 | def graphics(width = 10, height = 10, *args, &block) 70 | Processing::Graphics.new(width, height, *args).tap do |g| 71 | g.beginDraw {block.call g, g.getInternal__} if block 72 | end 73 | end 74 | 75 | def test_draw(*sources, width: 1000, height: 1000, pixelDensity: 1, label: nil) 76 | graphics(width, height, pixelDensity).tap do |g| 77 | g.renderMode :p5js 78 | g.beginDraw {g.instance_eval sources.compact.join("\n")} 79 | g.save draw_output_path(label, *sources) if label 80 | end 81 | end 82 | 83 | 84 | def assert_equal_vector(v1, v2, delta = 0.000001) 85 | assert_in_delta v1.x, v2.x, delta 86 | assert_in_delta v1.y, v2.y, delta 87 | assert_in_delta v1.z, v2.z, delta 88 | end 89 | 90 | def assert_equal_pixels(expected, actual, threshold: 1.0) 91 | exp_pixels = get_pixels expected 92 | act_pixels = get_pixels actual 93 | raise "Number of pixels does not match" if act_pixels.size != exp_pixels.size 94 | 95 | equal_count = exp_pixels.zip(act_pixels).count {|a, b| a == b} 96 | equal_rate = equal_count.to_f / act_pixels.size.to_f 97 | assert equal_rate >= threshold, <<~EOS 98 | The rate of the same pixel #{equal_rate} is below the threshold #{threshold} 99 | EOS 100 | end 101 | 102 | def assert_equal_draw( 103 | *shared_header, expected, actual, default_header: DEFAULT_DRAW_HEADER, 104 | width: 1000, height: 1000, threshold: 1.0, label: test_label) 105 | 106 | e = test_draw default_header, *shared_header, expected, label: "#{label}_expected" 107 | a = test_draw default_header, *shared_header, actual, label: "#{label}_actual" 108 | 109 | assert_equal_pixels e, a, threshold: threshold 110 | end 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 | 120 | 123 | #{svg_xml} 124 | 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 | 134 | def assert_p5_draw( 135 | *sources, default_header: DEFAULT_DRAW_HEADER, 136 | width: 1000, height: 1000, threshold: 0.99, label: test_label, **kwargs) 137 | 138 | source = [default_header, *sources].compact.join("\n") 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 145 | 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 153 | actual = test_draw source, width: width, height: height, pixelDensity: pd 154 | actual.save path.sub('_expected', '_actual') 155 | 156 | assert_equal_pixels actual.loadImage(path), actual, threshold: threshold 157 | end 158 | 159 | def assert_p5_fill(*sources, **kwargs) 160 | assert_p5_draw 'noStroke', *sources, label: test_label, **kwargs 161 | end 162 | 163 | def assert_p5_stroke(*sources, **kwargs) 164 | assert_p5_draw 'noFill; stroke 0, 255, 0', *sources, label: test_label, **kwargs 165 | end 166 | 167 | def assert_p5_fill_stroke(*sources, **kwargs) 168 | assert_p5_draw 'stroke 0, 255, 0', *sources, label: test_label, **kwargs 169 | end 170 | 171 | 172 | require_relative 'browser' if test_with_browser? 173 | -------------------------------------------------------------------------------- /test/test_capture.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | return if win32? 5 | 6 | 7 | class TestCapture < Test::Unit::TestCase 8 | 9 | P = Processing 10 | 11 | def capture(*args, **kwargs) 12 | P::Capture.new(*args, **kwargs) 13 | end 14 | 15 | def test_inspect() 16 | assert_match %r|#|, capture.inspect 17 | end 18 | 19 | end# TestCapture 20 | -------------------------------------------------------------------------------- /test/test_color.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestColor < Test::Unit::TestCase 5 | 6 | P = Processing 7 | G = P::Graphics 8 | 9 | def test_rgb_color() 10 | g = graphics 11 | 12 | g.colorMode G::RGB, 255 13 | c = g.color 10, 20, 30, 40 14 | assert_equal 0x280a141e, c 15 | assert_equal [10, 20, 30, 40], [g.red(c), g.green(c), g.blue(c), g.alpha(c)] 16 | 17 | g.colorMode G::RGB, 1.0 18 | c = g.color 0.1, 0.2, 0.3, 0.4 19 | assert_equal 0x6619334c, c 20 | assert_in_delta 0.1, g.red(c), 1 / 256.0 21 | assert_in_delta 0.2, g.green(c), 1 / 256.0 22 | assert_in_delta 0.3, g.blue(c), 1 / 256.0 23 | assert_in_delta 0.4, g.alpha(c), 1 / 256.0 24 | 25 | g.colorMode G::RGB, 0.5 26 | c = g.color 0.1, 0.2, 0.3, 0.4 27 | assert_equal 0xcc336699, c 28 | assert_in_delta 0.1, g.red(c), 1 / 256.0 29 | assert_in_delta 0.2, g.green(c), 1 / 256.0 30 | assert_in_delta 0.3, g.blue(c), 1 / 256.0 31 | assert_in_delta 0.4, g.alpha(c), 1 / 256.0 32 | end 33 | 34 | def test_hsb_color() 35 | g = graphics 36 | 37 | g.colorMode G::HSB, 1.0 38 | c = g.color 0.1, 0.2, 0.3, 0.4 39 | assert_in_delta 0.1, g.hue(c), 1 / 256.0 40 | assert_in_delta 0.2, g.saturation(c), 1 / 256.0 41 | assert_in_delta 0.3, g.brightness(c), 1 / 256.0 42 | assert_in_delta 0.4, g.alpha(c), 1 / 256.0 43 | 44 | g.colorMode G::HSB, 0.5 45 | c = g.color 0.1, 0.2, 0.3, 0.4 46 | assert_in_delta 0.1, g.hue(c), 1 / 256.0 47 | assert_in_delta 0.2, g.saturation(c), 1 / 256.0 48 | assert_in_delta 0.3, g.brightness(c), 1 / 256.0 49 | assert_in_delta 0.4, g.alpha(c), 1 / 256.0 50 | end 51 | 52 | def test_parse_color() 53 | g = graphics 54 | 55 | assert_equal 0xff010203, g.color('#010203') 56 | assert_equal 0x04010203, g.color('#01020304') 57 | assert_equal 0xff112233, g.color('#123') 58 | assert_equal 0x44112233, g.color('#1234') 59 | assert_equal 0xff0000ff, g.color('blue') 60 | assert_equal 0x040000ff, g.color('blue', 4) 61 | end 62 | 63 | def test_color_codes() 64 | g = graphics 65 | 66 | assert_equal 0xffff0000, g.color(:red) 67 | assert_equal 0xff008000, g.color(:GREEN) 68 | assert_equal 0xff0000ff, g.color('blue') 69 | 70 | assert_raise(ArgumentError) {g.color :unknown} 71 | end 72 | 73 | def test_default_background_color() 74 | assert_p5_draw '', default_header: '', threshold: THRESHOLD_TO_BE_FIXED 75 | end 76 | 77 | def test_default_fill_color() 78 | assert_p5_draw <<~END, default_header: nil 79 | background 100 80 | noStroke 81 | rect 100, 100, 500, 500 82 | END 83 | end 84 | 85 | def test_default_stroke_color() 86 | assert_p5_draw <<~END, default_header: nil 87 | background 100 88 | noFill 89 | strokeWeight 50 90 | line 100, 100, 500, 500 91 | END 92 | end 93 | 94 | end# TestColor 95 | -------------------------------------------------------------------------------- /test/test_font.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestFont < Test::Unit::TestCase 5 | 6 | P = Processing 7 | 8 | def font(*args) 9 | P::Font.new(Rays::Font.new(*args)).tap {|font| 10 | def font.intern() 11 | getInternal__ 12 | end 13 | } 14 | end 15 | 16 | def test_initialize() 17 | assert_not_nil font(nil) .intern.name 18 | assert_equal 'Arial', font('Arial').intern.name 19 | 20 | assert_equal 12, font .intern.size 21 | assert_equal 10, font(nil, 10).intern.size 22 | end 23 | 24 | def test_size() 25 | f = font nil, 10 26 | id = f.intern.object_id 27 | 28 | assert_equal 10, f.intern.size 29 | 30 | f.setSize__ 11 31 | assert_equal 11, f.intern.size 32 | assert_not_equal id, f.intern.object_id 33 | 34 | f.setSize__ 10 35 | assert_equal 10, f.intern.size 36 | assert_equal id, f.intern.object_id 37 | end 38 | 39 | def test_inspect() 40 | assert_match %r|#|, font.inspect 41 | end 42 | 43 | def test_list() 44 | assert_not P::Font.list.empty? 45 | end 46 | 47 | end# TestFont 48 | -------------------------------------------------------------------------------- /test/test_graphics.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestGraphics < Test::Unit::TestCase 5 | 6 | def test_beginDraw() 7 | g = graphics 8 | g.beginDraw 9 | assert_raise {g.beginDraw} 10 | end 11 | 12 | def test_save() 13 | g = graphics 100, 100 14 | g.beginDraw do 15 | g.background 200 16 | g.fill 255 17 | g.stroke 0 18 | g.ellipseMode :corner 19 | g.ellipse 0, 0, g.width, g.height 20 | end 21 | temp_path(ext: '.png') do |path| 22 | assert_nothing_raised {g.save path} 23 | assert_equal_pixels g, g.loadImage(path) 24 | end 25 | end 26 | 27 | def test_inspect() 28 | assert_match %r|#|, graphics.inspect 29 | end 30 | 31 | end# TestGraphics 32 | -------------------------------------------------------------------------------- /test/test_image.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestImage < Test::Unit::TestCase 5 | 6 | P = Processing 7 | 8 | def image(w = 10, h = 10, &block) 9 | img = Rays::Image.new w, h 10 | img.paint(&block) if block 11 | P::Image.new img 12 | end 13 | 14 | def test_set_color() 15 | g = graphics 16 | i = image(2, 2) {fill 0; rect 0, 0, 1, 1} 17 | 18 | assert_equal g.color(0, 0, 0), i.get(0, 0) 19 | 20 | i.set 0, 0, g.color(0, 255, 0) 21 | assert_equal g.color(0, 255, 0), i.get(0, 0) 22 | 23 | i.set 0, 0, g.color(0, 0, 255) 24 | assert_equal g.color(0, 0, 255), i.get(0, 0) 25 | end 26 | 27 | def test_get_color() 28 | g = graphics 29 | i = image 2, 2 do 30 | fill 1, 0, 0; rect 0, 0, 1, 1 31 | fill 0, 1, 0; rect 1, 0, 1, 1 32 | fill 0, 0, 1; rect 0, 1, 1, 1 33 | end 34 | 35 | assert_equal g.color(255, 0, 0), i.get(0, 0) 36 | assert_equal g.color(0, 255, 0), i.get(1, 0) 37 | assert_equal g.color(0, 0, 255), i.get(0, 1) 38 | end 39 | 40 | def test_pixels() 41 | i = image 2, 2 42 | 43 | i.loadPixels 44 | assert_equal [0] * 4, i.pixels 45 | assert_equal [0] * 4, i.getInternal__.pixels 46 | 47 | i.pixels.replace [0xffff0000, 0xff00ff00, 0xff0000ff, 0xff000000] 48 | assert_equal [0xffff0000, 0xff00ff00, 0xff0000ff, 0xff000000], i.pixels 49 | assert_equal [0] * 4, i.getInternal__.pixels 50 | 51 | i.updatePixels 52 | assert_nil i.pixels 53 | assert_equal [0xffff0000, 0xff00ff00, 0xff0000ff, 0xff000000], i.getInternal__.pixels 54 | assert_nothing_raised {i.updatePixels} 55 | 56 | i.loadPixels 57 | i.pixels.replace [0xff000000] 58 | assert_raise(ArgumentError) {i.updatePixels} 59 | end 60 | 61 | def test_inspect() 62 | assert_match %r|#|, image.inspect 63 | end 64 | 65 | end# TestImage 66 | -------------------------------------------------------------------------------- /test/test_shader.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestShader < Test::Unit::TestCase 5 | 6 | def color(*args) 7 | Rays::Color.new(*args) 8 | end 9 | 10 | def shader(vs = vshader, fs = fshader) 11 | Processing::Shader.new vs, fs 12 | end 13 | 14 | def vshader() 15 | "void main() {gl_Position = vec4(0.0, 0.0, 0.0, 1.0);}" 16 | end 17 | 18 | def fshader() 19 | "void main() {gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);}" 20 | end 21 | 22 | def test_initialize() 23 | assert shader vshader, fshader 24 | assert shader nil, fshader 25 | 26 | assert_raise(ArgumentError) {shader "", fshader} 27 | assert_raise(ArgumentError) {shader vshader, ""} 28 | assert_raise(ArgumentError) {shader vshader, nil} 29 | end 30 | 31 | def test_uniform() 32 | sh = shader(nil, <<~END).tap {|s| s.set :val, 1.0} 33 | uniform float val; 34 | void main() {gl_FragColor = vec4(val, 0.0, 0.0, 1.0);} 35 | END 36 | 37 | graphics do |g, image| 38 | g.shader sh 39 | g.rect 0, 0, 10, 10 40 | assert_equal color(1, 0, 0, 1), image[0, 0] 41 | end 42 | end 43 | 44 | def test_shadertoy_shader() 45 | assert_equal <<~EXPECTED, shader(nil, <<~ACTUAL).instance_variable_get(:@shader).fragment_shader_source 46 | void mainImage(out vec4 fragColor, in vec2 fragCoord) { 47 | } 48 | varying vec4 vertTexCoord; 49 | void main() { 50 | mainImage(gl_FragColor, vertTexCoord.xy); 51 | } 52 | EXPECTED 53 | void mainImage(out vec4 fragColor, in vec2 fragCoord) { 54 | } 55 | ACTUAL 56 | end 57 | 58 | def test_inspect() 59 | assert_match %r|#|, shader.inspect 60 | end 61 | 62 | end# TestShader 63 | -------------------------------------------------------------------------------- /test/test_shape.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestShape < Test::Unit::TestCase 5 | 6 | P = Processing 7 | G = P::GraphicsContext 8 | 9 | def createShape(kind = nil, *args, g: graphics) 10 | g.createShape kind, *args 11 | end 12 | 13 | def vec(*args) 14 | P::Vector.new(*args) 15 | end 16 | 17 | def test_size() 18 | assert_equal [0, 0], createShape .then {|s| [s.w, s.h]} 19 | assert_equal [30, 50], createShape(G::RECT, 10, 20, 30, 50).then {|s| [s.w, s.h]} 20 | assert_equal [20, 30], createShape(G::LINE, 10, 20, 30, 50).then {|s| [s.w, s.h]} 21 | end 22 | 23 | def test_visibility() 24 | gfx = graphics 100, 100 do |g| 25 | g.background 0 26 | end 27 | assert_equal 1, get_pixels(gfx).uniq.size 28 | 29 | gfx = graphics 100, 100 do |g| 30 | s = createShape G::RECT, 10, 20, 30, 40, g: g 31 | assert_true s.isVisible 32 | assert_true s.visible? 33 | 34 | g.background 0 35 | g.fill 255 36 | g.noStroke 37 | g.shape s 38 | end 39 | assert_equal 2, get_pixels(gfx).uniq.size 40 | 41 | gfx = graphics 100, 100 do |g| 42 | s = createShape G::RECT, 10, 20, 30, 40, g: g 43 | s.setVisible false 44 | assert_false s.isVisible 45 | assert_false s.visible? 46 | 47 | g.background 0 48 | g.fill 255 49 | g.noStroke 50 | g.shape s 51 | end 52 | assert_equal 1, get_pixels(gfx).uniq.size 53 | end 54 | 55 | def test_beginShape_points() 56 | assert_equal_draw_vertices 'strokeWeight 200', <<~END 57 | s.beginShape POINTS 58 | s.vertex 200, 300 59 | s.vertex 500, 600 60 | s.endShape 61 | END 62 | end 63 | 64 | def test_beginShape_lines() 65 | assert_equal_draw_vertices 'strokeWeight 200', <<~END 66 | s.beginShape LINES 67 | s.vertex 100, 200 68 | s.vertex 300, 400 69 | s.vertex 500, 600 70 | s.vertex 700, 800 71 | s.endShape 72 | END 73 | end 74 | 75 | def test_beginShape_triangles() 76 | assert_equal_draw_vertices <<~END 77 | s.beginShape TRIANGLES 78 | s.vertex 100, 100 79 | s.vertex 100, 500 80 | s.vertex 400, 200 81 | s.vertex 500, 100 82 | s.vertex 500, 500 83 | s.vertex 900, 200 84 | s.endShape 85 | END 86 | end 87 | 88 | def test_beginShape_triangle_fan() 89 | assert_equal_draw_vertices <<~END 90 | s.beginShape TRIANGLE_FAN 91 | s.vertex 100, 100 92 | s.vertex 100, 500 93 | s.vertex 200, 600 94 | s.vertex 300, 500 95 | s.vertex 400, 600 96 | s.vertex 500, 200 97 | s.vertex 400, 100 98 | s.endShape 99 | END 100 | end 101 | 102 | def test_beginShape_triangle_strip() 103 | assert_equal_draw_vertices <<~END 104 | s.beginShape TRIANGLE_STRIP 105 | s.vertex 100, 100 106 | s.vertex 100, 900 107 | s.vertex 500, 200 108 | s.vertex 500, 800 109 | s.vertex 900, 100 110 | s.vertex 900, 900 111 | s.endShape 112 | END 113 | end 114 | 115 | def test_beginShape_quads() 116 | assert_equal_draw_vertices <<~END 117 | s.beginShape QUADS 118 | s.vertex 100, 100 119 | s.vertex 200, 500 120 | s.vertex 300, 400 121 | s.vertex 400, 200 122 | s.vertex 500, 100 123 | s.vertex 600, 500 124 | s.vertex 700, 400 125 | s.vertex 800, 200 126 | s.endShape 127 | END 128 | end 129 | 130 | def test_beginShape_quad_strip() 131 | assert_equal_draw_vertices <<~END 132 | s.beginShape QUAD_STRIP 133 | s.vertex 100, 100 134 | s.vertex 100, 500 135 | s.vertex 500, 200 136 | s.vertex 500, 400 137 | s.vertex 900, 100 138 | s.vertex 900, 500 139 | s.endShape 140 | END 141 | end 142 | 143 | def test_beginShape_tess() 144 | assert_equal_draw_vertices <<~END 145 | s.beginShape TESS 146 | s.vertex 100, 100 147 | s.vertex 100, 500 148 | s.vertex 500, 500 149 | s.vertex 500, 400 150 | s.vertex 300, 400 151 | s.vertex 300, 300 152 | s.vertex 500, 300 153 | s.vertex 500, 100 154 | s.endShape 155 | END 156 | end 157 | 158 | def test_beginShape_polygon() 159 | assert_equal_draw_vertices <<~END 160 | s.beginShape 161 | s.vertex 100, 100 162 | s.vertex 100, 500 163 | s.vertex 500, 500 164 | s.vertex 500, 400 165 | s.vertex 300, 400 166 | s.vertex 300, 300 167 | s.vertex 500, 300 168 | s.vertex 500, 100 169 | s.endShape 170 | END 171 | end 172 | 173 | def test_beginShape_twice() 174 | first, second = <<~FIRST, <<~SECOND 175 | s.vertex 100, 100 176 | s.vertex 100, 500 177 | s.vertex 400, 500 178 | s.vertex 400, 300 179 | FIRST 180 | s.vertex 500, 300 181 | s.vertex 500, 500 182 | s.vertex 800, 500 183 | s.vertex 800, 100 184 | SECOND 185 | 186 | assert_equal_draw <<~EXPECTED, <<~ACTUAL 187 | s = self 188 | s.beginShape 189 | #{first} 190 | #{second} 191 | s.endShape 192 | EXPECTED 193 | s = createShape 194 | s.beginShape 195 | #{first} 196 | s.endShape 197 | s.beginShape 198 | #{second} 199 | s.endShape 200 | shape s 201 | ACTUAL 202 | 203 | assert_equal_draw <<~EXPECTED, <<~ACTUAL 204 | s = self 205 | s.beginShape 206 | #{first} 207 | #{second} 208 | s.endShape CLOSE 209 | EXPECTED 210 | s = createShape 211 | s.beginShape 212 | #{first} 213 | s.endShape 214 | s.beginShape 215 | #{second} 216 | s.endShape CLOSE 217 | shape s 218 | ACTUAL 219 | 220 | assert_equal_draw <<~EXPECTED, <<~ACTUAL 221 | s = self 222 | s.beginShape 223 | #{first} 224 | #{second} 225 | s.endShape 226 | EXPECTED 227 | s = createShape 228 | s.beginShape TRIANGLES 229 | #{first} 230 | s.endShape 231 | s.beginShape 232 | #{second} 233 | s.endShape 234 | shape s 235 | ACTUAL 236 | end 237 | 238 | def test_curveVertex() 239 | assert_equal_draw_vertices <<~END 240 | s.beginShape 241 | s.curveVertex 100, 100 242 | s.curveVertex 800, 100 243 | s.curveVertex 800, 800 244 | s.curveVertex 100, 800 245 | s.endShape 246 | END 247 | 248 | assert_equal_draw_vertices <<~END 249 | s.beginShape 250 | s.curveVertex 200, 200 251 | s.curveVertex 200, 200 252 | s.curveVertex 800, 200 253 | s.curveVertex 800, 400 254 | s.curveVertex 200, 400 255 | s.curveVertex 200, 800 256 | s.curveVertex 800, 800 257 | s.curveVertex 800, 700 258 | s.curveVertex 800, 700 259 | s.endShape 260 | END 261 | end 262 | 263 | def test_bezierVertex() 264 | assert_equal_draw_vertices <<~END 265 | s.beginShape 266 | s.vertex 100, 100 267 | s.bezierVertex 900, 100, 900, 900, 200, 500 268 | s.endShape 269 | END 270 | 271 | assert_equal_draw_vertices <<~END 272 | s.beginShape 273 | s.vertex 100, 100 274 | s.bezierVertex 900, 100, 900, 500, 300, 500 275 | s.bezierVertex 100, 900, 900, 900, 900, 600 276 | s.endShape 277 | END 278 | end 279 | 280 | def test_quadraticVertex() 281 | assert_equal_draw_vertices <<~END 282 | s.beginShape 283 | s.vertex 100, 100 284 | s.quadraticVertex 800, 500, 200, 800 285 | s.endShape 286 | END 287 | 288 | assert_equal_draw_vertices <<~END 289 | s.beginShape 290 | s.vertex 100, 100 291 | s.quadraticVertex 800, 100, 500, 500 292 | s.quadraticVertex 100, 800, 800, 800 293 | s.endShape 294 | END 295 | end 296 | 297 | def test_contour() 298 | assert_equal_draw_vertices <<~END 299 | s.beginShape 300 | s.vertex 100, 100 301 | s.vertex 100, 900 302 | s.vertex 900, 900 303 | s.vertex 900, 100 304 | s.beginContour 305 | s.vertex 200, 200 306 | s.vertex 800, 200 307 | s.vertex 700, 700 308 | s.vertex 200, 800 309 | s.endContour 310 | s.endShape 311 | END 312 | end 313 | 314 | def test_getVertex() 315 | s = createShape 316 | s.beginShape 317 | s.vertex 1, 2 318 | s.vertex 3, 4 319 | s.vertex 5, 6 320 | s.endShape 321 | 322 | assert_equal vec(1, 2), s.getVertex(0) 323 | assert_equal vec(3, 4), s.getVertex(1) 324 | assert_equal vec(5, 6), s.getVertex(-1) 325 | assert_equal vec(3, 4), s.getVertex(-2) 326 | end 327 | 328 | def test_setVertex() 329 | s = createShape 330 | s.beginShape 331 | s.vertex 1, 2 332 | s.vertex 3, 4 333 | s.vertex 5, 6 334 | s.endShape 335 | 336 | points = -> do 337 | s.getVertexCount.times.map {|i| 338 | s.getVertex(i).then {|v| [v.x, v.y]} 339 | }.flatten 340 | end 341 | 342 | s.setVertex(0, vec(7, 8)) 343 | assert_equal [7, 8, 3, 4, 5, 6], points.call 344 | 345 | s.setVertex(-1, vec(9, 10)) 346 | assert_equal [7, 8, 3, 4, 9, 10], points.call 347 | end 348 | 349 | def test_getVertexCount() 350 | s = createShape 351 | s.beginShape G::TRIANGLES 352 | assert_equal 0, s.getVertexCount 353 | 354 | s.vertex 1, 2 355 | assert_equal 1, s.getVertexCount 356 | 357 | s.vertex 3, 4 358 | assert_equal 2, s.getVertexCount 359 | 360 | s.vertex 5, 6 361 | assert_equal 3, s.getVertexCount 362 | 363 | s.endShape 364 | assert_equal 3, s.getVertexCount 365 | end 366 | 367 | def test_fill() 368 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 369 | noFill 370 | noStroke 371 | HEADER 372 | fill 0, 0, 255 373 | rect 100, 100, 500, 400 374 | EXPECTED 375 | s = createShape 376 | s.beginShape 377 | s.fill 0, 0, 255 378 | s.vertex 100, 100 379 | s.vertex 600, 100 380 | s.vertex 600, 500 381 | s.vertex 100, 500 382 | s.endShape 383 | shape s 384 | ACTUAL 385 | 386 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 387 | noFill 388 | noStroke 389 | HEADER 390 | fill 0, 0, 255 391 | rect 100, 100, 500, 400 392 | EXPECTED 393 | fill 0, 0, 255 394 | s = createShape 395 | s.beginShape 396 | s.vertex 100, 100 397 | s.vertex 600, 100 398 | s.vertex 600, 500 399 | s.vertex 100, 500 400 | s.endShape 401 | shape s 402 | ACTUAL 403 | end 404 | 405 | def test_stroke() 406 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 407 | noFill 408 | noStroke 409 | HEADER 410 | stroke 0, 0, 255 411 | rect 100, 100, 500, 400 412 | EXPECTED 413 | s = createShape 414 | s.beginShape 415 | s.stroke 0, 0, 255 416 | s.vertex 100, 100 417 | s.vertex 600, 100 418 | s.vertex 600, 500 419 | s.vertex 100, 500 420 | s.endShape CLOSE 421 | shape s 422 | ACTUAL 423 | 424 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 425 | noFill 426 | noStroke 427 | HEADER 428 | stroke 0, 0, 255 429 | rect 100, 100, 500, 400 430 | EXPECTED 431 | stroke 0, 0, 255 432 | s = createShape 433 | s.beginShape 434 | s.vertex 100, 100 435 | s.vertex 600, 100 436 | s.vertex 600, 500 437 | s.vertex 100, 500 438 | s.endShape CLOSE 439 | shape s 440 | ACTUAL 441 | end 442 | 443 | def test_setFill() 444 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 445 | noFill 446 | noStroke 447 | HEADER 448 | fill 0, 0, 255 449 | rect 100, 100, 500, 400 450 | EXPECTED 451 | s = createShape 452 | s.beginShape 453 | s.vertex 100, 100 454 | s.vertex 600, 100 455 | s.vertex 600, 500 456 | s.vertex 100, 500 457 | s.endShape 458 | s.setFill 0, 0, 255 459 | shape s 460 | ACTUAL 461 | 462 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 463 | noFill 464 | noStroke 465 | HEADER 466 | fill 0, 0, 255 467 | ellipse 300, 400, 500, 400 468 | EXPECTED 469 | s = createShape ELLIPSE, 300, 400, 500, 400 470 | s.setFill 0, 0, 255 471 | shape s 472 | ACTUAL 473 | end 474 | 475 | def test_setStroke() 476 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 477 | noFill 478 | noStroke 479 | HEADER 480 | stroke 0, 0, 255 481 | rect 100, 100, 500, 400 482 | EXPECTED 483 | s = createShape 484 | s.beginShape 485 | s.vertex 100, 100 486 | s.vertex 600, 100 487 | s.vertex 600, 500 488 | s.vertex 100, 500 489 | s.endShape CLOSE 490 | s.setStroke 0, 0, 255 491 | shape s 492 | ACTUAL 493 | 494 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 495 | noFill 496 | noStroke 497 | HEADER 498 | stroke 0, 0, 255 499 | ellipse 300, 400, 500, 400 500 | EXPECTED 501 | s = createShape ELLIPSE, 300, 400, 500, 400 502 | s.setStroke 0, 0, 255 503 | shape s 504 | ACTUAL 505 | end 506 | 507 | def test_setStrokeWeight() 508 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 509 | noFill 510 | strokeWeight 1 511 | HEADER 512 | strokeWeight 100 513 | rect 100, 100, 500, 400 514 | EXPECTED 515 | s = createShape 516 | s.beginShape 517 | s.vertex 100, 100 518 | s.vertex 600, 100 519 | s.vertex 600, 500 520 | s.vertex 100, 500 521 | s.endShape CLOSE 522 | s.setStrokeWeight 100 523 | shape s 524 | ACTUAL 525 | 526 | assert_equal_draw <<~HEADER, <<~EXPECTED, <<~ACTUAL 527 | noFill 528 | strokeWeight 1 529 | HEADER 530 | strokeWeight 100 531 | ellipse 300, 400, 500, 400 532 | EXPECTED 533 | s = createShape ELLIPSE, 300, 400, 500, 400 534 | s.setStrokeWeight 100 535 | shape s 536 | ACTUAL 537 | end 538 | 539 | def test_addChild() 540 | group = createShape G::GROUP 541 | assert_nil group.getChild(0) 542 | 543 | rect = createShape G::RECT, 100, 100, 200, 300 544 | group.addChild rect 545 | assert_equal 1, group.getChildCount 546 | assert_equal rect, group.getChild(0) 547 | end 548 | 549 | def test_addChild_to_non_group_shape() 550 | s = createShape 551 | assert_equal 0, s.getChildCount 552 | 553 | assert_nothing_raised { 554 | s.addChild createShape(G::RECT, 100, 100, 200, 300) 555 | } 556 | assert_equal 0, s.getChildCount 557 | end 558 | 559 | def test_getChildCount() 560 | s = createShape G::GROUP 561 | assert_equal 0, s.getChildCount 562 | 563 | s.addChild createShape 564 | assert_equal 1, s.getChildCount 565 | 566 | s.addChild createShape 567 | assert_equal 2, s.getChildCount 568 | end 569 | 570 | def test_translate() 571 | assert_equal_draw <<~EXPECTED, <<~ACTUAL 572 | translate 100, 200 573 | rect 0, 0, 300, 400 574 | EXPECTED 575 | s = createShape RECT, 0, 0, 300, 400 576 | s.translate 100, 200 577 | shape s 578 | ACTUAL 579 | end 580 | 581 | def test_rotate() 582 | assert_equal_draw <<~EXPECTED, <<~ACTUAL 583 | rotate PI / 10 584 | rect 100, 200, 300, 400 585 | EXPECTED 586 | s = createShape RECT, 100, 200, 300, 400 587 | s.rotate PI / 10 588 | shape s 589 | ACTUAL 590 | end 591 | 592 | def test_scale() 593 | assert_equal_draw <<~EXPECTED, <<~ACTUAL 594 | scale 0.5, 0.6 595 | rect 100, 200, 300, 400 596 | EXPECTED 597 | s = createShape RECT, 100, 200, 300, 400 598 | s.scale 0.5, 0.6 599 | shape s 600 | ACTUAL 601 | end 602 | 603 | def test_sequential_transformation() 604 | assert_equal_draw <<~EXPECTED, <<~ACTUAL 605 | translate 100, 200 606 | rotate PI / 4 607 | scale 0.5, 0.6 608 | translate 100, 200 609 | rotate(-PI / 4) 610 | scale 2, 3 611 | rect 0, 0, 300, 400 612 | EXPECTED 613 | s = createShape RECT, 0, 0, 300, 400 614 | s.translate 100, 200 615 | s.rotate PI / 4 616 | s.scale 0.5, 0.6 617 | s.translate 100, 200 618 | s.rotate(-PI / 4) 619 | s.scale 2, 3 620 | shape s 621 | ACTUAL 622 | end 623 | 624 | def assert_equal_draw_vertices(*shared_header, source) 625 | assert_equal_draw(*shared_header, <<~EXPECTED, <<~ACTUAL, label: test_label) 626 | s = self 627 | #{source} 628 | EXPECTED 629 | s = createShape 630 | #{source} 631 | shape s 632 | ACTUAL 633 | end 634 | 635 | end# TestShape 636 | -------------------------------------------------------------------------------- /test/test_svg.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestSVG < Test::Unit::TestCase 5 | 6 | def test_color_code() 7 | assert_svg_draw '' 8 | assert_svg_draw '' 9 | assert_svg_draw '' 10 | assert_svg_draw '' 11 | assert_svg_draw '' 12 | end 13 | 14 | def test_fill() 15 | assert_svg_draw '' 16 | end 17 | 18 | def test_stroke_and_width() 19 | assert_svg_draw <<~END 20 | 22 | END 23 | end 24 | 25 | def test_strokeCap() 26 | assert_svg_draw <<~END 27 | 29 | END 30 | assert_svg_draw <<~END 31 | 33 | END 34 | assert_svg_draw <<~END 35 | 37 | END 38 | end 39 | 40 | def test_strokeJoin() 41 | assert_svg_draw <<~END 42 | 44 | END 45 | assert_svg_draw <<~END 46 | 48 | END 49 | assert_svg_draw <<~END 50 | 52 | END 53 | assert_svg_draw <<~END 54 | 56 | END 57 | assert_svg_draw <<~END 58 | 60 | END 61 | end 62 | 63 | def test_line() 64 | assert_svg_draw <<~END 65 | 66 | END 67 | end 68 | 69 | def test_rect() 70 | assert_svg_draw '' 71 | assert_svg_draw '' 72 | assert_svg_draw '' 73 | 74 | assert_svg_draw <<~END, threshold: THRESHOLD_TO_BE_FIXED 75 | 76 | END 77 | assert_svg_draw <<~END, threshold: THRESHOLD_TO_BE_FIXED 78 | 79 | END 80 | end 81 | 82 | def test_circle() 83 | assert_svg_draw '' 84 | end 85 | 86 | def test_ellipse() 87 | assert_svg_draw '' 88 | end 89 | 90 | def test_polyline() 91 | assert_svg_draw <<~END 92 | 93 | END 94 | assert_svg_draw <<~END 95 | 97 | END 98 | end 99 | 100 | def test_polygon() 101 | assert_svg_draw <<~END 102 | 103 | END 104 | assert_svg_draw <<~END 105 | 107 | END 108 | end 109 | 110 | def test_path_Mm() 111 | assert_svg_draw path_xml "M 200,300 L 500,400" 112 | assert_svg_draw path_xml "m 200,300 L 500,400" 113 | assert_svg_draw path_xml "M 200,300 L 500,400 M 600,700 L 300,800" 114 | assert_svg_draw path_xml "M 200,300 L 500,400 m 100,300 L 300,800" 115 | assert_svg_draw path_xml "M 200,300 500,400 600,700 300,800" 116 | assert_svg_draw path_xml "m 200,300 300,100 100,300 -300,100" 117 | end 118 | 119 | def test_path_Ll() 120 | assert_svg_draw path_xml "M 200,300 L 500,400 L 600,700 L 300,800" 121 | assert_svg_draw path_xml "M 200,300 L 500,400 600,700 300,800" 122 | assert_svg_draw path_xml "M 200,300 l 300,100 l 100,300 l -300,100" 123 | assert_svg_draw path_xml "M 200,300 l 300,100 100,300 -300,100" 124 | end 125 | 126 | def test_path_Hh() 127 | assert_svg_draw path_xml "M 200,300 H 500 H 800" 128 | assert_svg_draw path_xml "M 200,300 H 500 800" 129 | assert_svg_draw path_xml "M 200,300 h 300 h 300" 130 | assert_svg_draw path_xml "M 200,300 h 300 300" 131 | end 132 | 133 | def test_path_Vv() 134 | assert_svg_draw path_xml "M 200,300 V 500 V 800" 135 | assert_svg_draw path_xml "M 200,300 V 500 800" 136 | assert_svg_draw path_xml "M 200,300 v 200 h 300" 137 | assert_svg_draw path_xml "M 200,300 h 200 300" 138 | end 139 | 140 | def test_path_Qq() 141 | assert_svg_draw path_xml "M 200,100 Q 800,300 300,400 Q 400,800 800,800" 142 | assert_svg_draw path_xml "M 200,100 Q 800,300 300,400 400,800 800,800" 143 | assert_svg_draw path_xml "M 200,100 q 600,200 100,300 q 100,400 500,400" 144 | assert_svg_draw path_xml "M 200,100 q 600,200 100,300 100,400 500,400" 145 | end 146 | 147 | def test_path_Tt() 148 | assert_svg_draw path_xml "M 200,100 T 300,400 T 800,800" 149 | assert_svg_draw path_xml "M 200,100 T 300,400 800,800" 150 | assert_svg_draw path_xml "M 200,100 t 100,300 t 500,400" 151 | assert_svg_draw path_xml "M 200,100 t 100,300 500,400" 152 | end 153 | 154 | def test_path_Cc() 155 | assert_svg_draw path_xml "M 200,100 C 800,200 300,300 700,400 C 600,800 300,500 200,700" 156 | assert_svg_draw path_xml "M 200,100 C 800,200 300,300 700,400 600,800 300,500 200,700" 157 | assert_svg_draw path_xml "M 200,100 c 600,100 100,200 500,300 c -100,400 -400,100 -500,300" 158 | assert_svg_draw path_xml "M 200,100 c 600,100 100,200 500,300 -100,400 -400,100 -500,300" 159 | end 160 | 161 | def test_path_Ss() 162 | assert_svg_draw path_xml "M 200,100 S 800,300 500,500 S 800,700 200,900" 163 | assert_svg_draw path_xml "M 200,100 S 800,300 500,500 800,700 200,900" 164 | assert_svg_draw path_xml "M 200,100 s 600,200 300,400 s 300,200 -300,400" 165 | assert_svg_draw path_xml "M 200,100 s 600,200 300,400 300,200 -300,400" 166 | end 167 | 168 | def test_path_Aa() 169 | assert_svg_draw path_xml(<<~END), threshold: THRESHOLD_TO_BE_FIXED 170 | M 200,100 A 300,200 0 0 0 500,400 A 600,500 0 0 0 900,800 171 | END 172 | assert_svg_draw path_xml(<<~END), threshold: THRESHOLD_TO_BE_FIXED 173 | M 200,100 A 300,200 0 0 0 500,400 600,500 0 0 0 900,800 174 | END 175 | assert_svg_draw path_xml(<<~END), threshold: THRESHOLD_TO_BE_FIXED 176 | M 200,100 a 100,100 0 0 0 300,300 a 100,100 0 0 0 400,400 177 | END 178 | assert_svg_draw path_xml(<<~END), threshold: THRESHOLD_TO_BE_FIXED 179 | M 200,100 a 100,100 0 0 0 300,300 100,100 0 0 0 400,400 180 | END 181 | end 182 | 183 | def test_path_Zz() 184 | assert_svg_draw path_xml "M 200,300 L 500,400 L 600,700 L 300,800 Z" 185 | assert_svg_draw path_xml "M 200,300 L 500,400 L 600,700 L 300,800 z" 186 | end 187 | 188 | def test_group_fill() 189 | assert_svg_draw <<~END 190 | 191 | 192 | 193 | END 194 | assert_svg_draw <<~END 195 | 196 | 197 | 198 | END 199 | assert_svg_draw <<~END 200 | 201 | 202 | 203 | END 204 | end 205 | 206 | def test_group_stroke() 207 | assert_svg_draw <<~END 208 | 209 | 211 | 212 | END 213 | assert_svg_draw <<~END 214 | 215 | 217 | 218 | END 219 | assert_svg_draw <<~END 220 | 221 | 223 | 224 | END 225 | end 226 | 227 | def test_group_stroke_width() 228 | assert_svg_draw <<~END 229 | 230 | 231 | 232 | END 233 | assert_svg_draw <<~END 234 | 235 | 236 | 237 | END 238 | assert_svg_draw <<~END 239 | 240 | 241 | 242 | END 243 | end 244 | 245 | def test_path_data_shorthand() 246 | assert_svg_draw path_xml "M200,300L500,400L600,700L300,800" 247 | assert_svg_draw path_xml "M 200,300 l 500.99.400 0,200 -200,0.99.300,200" 248 | assert_svg_draw path_xml "M 200,800 l 500-200-200-200-300-300" 249 | end 250 | 251 | def path_xml(d) 252 | <<~END 253 | 254 | END 255 | end 256 | 257 | end# TestSVG 258 | -------------------------------------------------------------------------------- /test/test_text_bounds.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestTextBounds < Test::Unit::TestCase 5 | 6 | P = Processing 7 | 8 | def bounds(*args) 9 | P::TextBounds.new(*args) 10 | end 11 | 12 | def test_inspect() 13 | assert_equal "#", bounds(1, 2, 3, 4).inspect 14 | end 15 | 16 | end# TestTextBounds 17 | -------------------------------------------------------------------------------- /test/test_touch.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestTouch < Test::Unit::TestCase 5 | 6 | P = Processing 7 | 8 | def touch(id, x, y) 9 | P::Touch.new id, x, y 10 | end 11 | 12 | def test_inspect() 13 | assert_equal "#", touch(1, 2, 3).inspect 14 | end 15 | 16 | end# TestTouch 17 | -------------------------------------------------------------------------------- /test/test_utility.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestUtility < Test::Unit::TestCase 5 | 6 | P = Processing 7 | 8 | include P::GraphicsContext 9 | 10 | def test_createVector() 11 | assert_equal P::Vector, createVector(1, 2).class 12 | end 13 | 14 | def test_createShader() 15 | fs = "void main() {gl_FragColor = vec4(1.0);}" 16 | assert_equal P::Shader, createShader(nil, fs).class 17 | end 18 | 19 | end# TestUtility 20 | -------------------------------------------------------------------------------- /test/test_vector.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helper' 2 | 3 | 4 | class TestVector < Test::Unit::TestCase 5 | 6 | P = Processing 7 | V = P::Vector 8 | M = Math 9 | PI = M::PI 10 | 11 | def vec(*args, **kwargs) 12 | V.new(*args, **kwargs) 13 | end 14 | 15 | def point(*args, **kwargs) 16 | Rays::Point.new(*args, **kwargs) 17 | end 18 | 19 | def test_initialize() 20 | assert_equal_vector vec(0, 0, 0), vec() 21 | assert_equal_vector vec(1, 0, 0), vec(1) 22 | assert_equal_vector vec(1, 2, 0), vec(1, 2) 23 | assert_equal_vector vec(1, 2, 3), vec(1, 2, 3) 24 | 25 | assert_equal_vector vec(0, 0, 0), vec([]) 26 | assert_equal_vector vec(1, 0, 0), vec([1]) 27 | assert_equal_vector vec(1, 2, 0), vec([1, 2]) 28 | assert_equal_vector vec(1, 2, 3), vec([1, 2, 3]) 29 | 30 | assert_equal_vector vec(1, 2, 3), vec(vec 1, 2, 3) 31 | assert_equal_vector vec(1, 2, 3), vec(point 1, 2, 3) 32 | end 33 | 34 | def test_set() 35 | v0 = vec 9, 9, 9 36 | 37 | v = v0.dup; v.set; assert_equal_vector vec(0, 0, 0), v 38 | v = v0.dup; v.set 1; assert_equal_vector vec(1, 0, 0), v 39 | v = v0.dup; v.set 1, 2; assert_equal_vector vec(1, 2, 0), v 40 | v = v0.dup; v.set 1, 2, 3; assert_equal_vector vec(1, 2, 3), v 41 | 42 | v = v0.dup; v.set []; assert_equal_vector vec(0, 0, 0), v 43 | v = v0.dup; v.set [1]; assert_equal_vector vec(1, 0, 0), v 44 | v = v0.dup; v.set [1, 2]; assert_equal_vector vec(1, 2, 0), v 45 | v = v0.dup; v.set [1, 2, 3]; assert_equal_vector vec(1, 2, 3), v 46 | 47 | v = v0.dup; v.set vec(1, 2, 3); assert_equal_vector vec(1, 2, 3), v 48 | v = v0.dup; v.set point(1, 2, 3); assert_equal_vector vec(1, 2, 3), v 49 | end 50 | 51 | def test_dup() 52 | v1 = vec 1, 2, 3 53 | assert_equal_vector vec(1, 2, 3), v1 54 | 55 | v2 = v1.dup 56 | assert_equal_vector vec(1, 2, 3), v1 57 | assert_equal_vector vec(1, 2, 3), v2 58 | 59 | v1.set 7, 8, 9 60 | assert_equal_vector vec(7, 8, 9), v1 61 | assert_equal_vector vec(1, 2, 3), v2 62 | end 63 | 64 | def test_copy() 65 | v1 = vec 1, 2, 3 66 | assert_equal_vector vec(1, 2, 3), v1 67 | 68 | v2 = v1.copy 69 | assert_equal_vector vec(1, 2, 3), v1 70 | assert_equal_vector vec(1, 2, 3), v2 71 | 72 | v1.set 7, 8, 9 73 | assert_equal_vector vec(7, 8, 9), v1 74 | assert_equal_vector vec(1, 2, 3), v2 75 | end 76 | 77 | def test_xyz() 78 | v = vec 1, 2, 3 79 | assert_equal_vector vec(1, 2, 3), v 80 | assert_equal [1, 2, 3], [v.x, v.y, v.z] 81 | 82 | v.x = 7 83 | assert_equal_vector vec(7, 2, 3), v 84 | 85 | v.y = 8 86 | assert_equal_vector vec(7, 8, 3), v 87 | 88 | v.z = 9 89 | assert_equal_vector vec(7, 8, 9), v 90 | end 91 | 92 | def test_array() 93 | assert_equal [1, 2, 3], vec(1, 2, 3).array 94 | end 95 | 96 | def test_add() 97 | v = vec 1, 2, 3 98 | v.add 4, 5, 6 99 | assert_equal_vector vec(5, 7, 9), v 100 | 101 | assert_equal_vector vec(1, 2, 3), vec(1, 2, 3).add() 102 | assert_equal_vector vec(5, 2, 3), vec(1, 2, 3).add(4) 103 | assert_equal_vector vec(5, 7, 3), vec(1, 2, 3).add(4, 5) 104 | assert_equal_vector vec(5, 7, 9), vec(1, 2, 3).add(4, 5, 6) 105 | 106 | assert_equal_vector vec(1, 2, 3), vec(1, 2, 3).add([]) 107 | assert_equal_vector vec(5, 2, 3), vec(1, 2, 3).add([4]) 108 | assert_equal_vector vec(5, 7, 3), vec(1, 2, 3).add([4, 5]) 109 | assert_equal_vector vec(5, 7, 9), vec(1, 2, 3).add([4, 5, 6]) 110 | 111 | assert_equal_vector vec(5, 7, 9), vec(1, 2, 3).add( vec(4, 5, 6)) 112 | assert_equal_vector vec(5, 7, 9), vec(1, 2, 3).add(point(4, 5, 6)) 113 | end 114 | 115 | def test_sub() 116 | v = vec 9, 8, 7 117 | v.sub 1, 2, 3 118 | assert_equal_vector vec(8, 6, 4), v 119 | 120 | assert_equal_vector vec(9, 8, 7), vec(9, 8, 7).sub() 121 | assert_equal_vector vec(8, 8, 7), vec(9, 8, 7).sub(1) 122 | assert_equal_vector vec(8, 6, 7), vec(9, 8, 7).sub(1, 2) 123 | assert_equal_vector vec(8, 6, 4), vec(9, 8, 7).sub(1, 2, 3) 124 | 125 | assert_equal_vector vec(9, 8, 7), vec(9, 8, 7).sub([]) 126 | assert_equal_vector vec(8, 8, 7), vec(9, 8, 7).sub([1]) 127 | assert_equal_vector vec(8, 6, 7), vec(9, 8, 7).sub([1, 2]) 128 | assert_equal_vector vec(8, 6, 4), vec(9, 8, 7).sub([1, 2, 3]) 129 | 130 | assert_equal_vector vec(8, 6, 4), vec(9, 8, 7).sub( vec(1, 2, 3)) 131 | assert_equal_vector vec(8, 6, 4), vec(9, 8, 7).sub(point(1, 2, 3)) 132 | end 133 | 134 | def test_mult() 135 | v = vec 1, 2, 3 136 | v.mult 2 137 | assert_equal_vector vec(2, 4, 6), v 138 | end 139 | 140 | def test_div() 141 | v = vec 2, 4, 6 142 | v.div 2 143 | assert_equal_vector vec(1, 2, 3), v 144 | end 145 | 146 | def test_op_add() 147 | v1 = vec 1, 2, 3 148 | v2 = vec 4, 5, 6 149 | assert_equal_vector vec(5, 7, 9), v1 + v2 150 | assert_equal_vector vec(1, 2, 3), v1 151 | assert_equal_vector vec(4, 5, 6), v2 152 | 153 | assert_equal_vector vec(5, 2, 3), vec(1, 2, 3) + 4 154 | 155 | assert_equal_vector vec(1, 2, 3), vec(1, 2, 3) + [] 156 | assert_equal_vector vec(5, 2, 3), vec(1, 2, 3) + [4] 157 | assert_equal_vector vec(5, 7, 3), vec(1, 2, 3) + [4, 5] 158 | assert_equal_vector vec(5, 7, 9), vec(1, 2, 3) + [4, 5, 6] 159 | 160 | assert_equal_vector vec(5, 7, 9), vec(1, 2, 3) + vec(4, 5, 6) 161 | assert_equal_vector vec(5, 7, 9), vec(1, 2, 3) + point(4, 5, 6) 162 | end 163 | 164 | def test_op_sub() 165 | v1 = vec 9, 8, 7 166 | v2 = vec 1, 2, 3 167 | assert_equal_vector vec(8, 6, 4), v1 - v2 168 | assert_equal_vector vec(9, 8, 7), v1 169 | assert_equal_vector vec(1, 2, 3), v2 170 | 171 | assert_equal_vector vec(8, 8, 7), vec(9, 8, 7) - 1 172 | 173 | assert_equal_vector vec(9, 8, 7), vec(9, 8, 7) - [] 174 | assert_equal_vector vec(8, 8, 7), vec(9, 8, 7) - [1] 175 | assert_equal_vector vec(8, 6, 7), vec(9, 8, 7) - [1, 2] 176 | assert_equal_vector vec(8, 6, 4), vec(9, 8, 7) - [1, 2, 3] 177 | 178 | assert_equal_vector vec(8, 6, 4), vec(9, 8, 7) - vec(1, 2, 3) 179 | assert_equal_vector vec(8, 6, 4), vec(9, 8, 7) - point(1, 2, 3) 180 | end 181 | 182 | def test_op_mult() 183 | v = vec 1, 2, 3 184 | assert_equal_vector vec(2, 4, 6), v * 2 185 | assert_equal_vector vec(1, 2, 3), v 186 | end 187 | 188 | def test_op_div() 189 | v = vec 2, 4, 6 190 | assert_equal_vector vec(1, 2, 3), v / 2 191 | assert_equal_vector vec(2, 4, 6), v 192 | end 193 | 194 | def test_op_negate() 195 | assert_equal_vector vec(-1, -2, -3), -vec( 1, 2, 3) 196 | assert_equal_vector vec( 1, 2, 3), -vec(-1, -2, -3) 197 | end 198 | 199 | def test_fun_add() 200 | v1 = vec 1, 2, 3 201 | v2 = vec 4, 5, 6 202 | result = vec 203 | assert_equal_vector vec(5, 7, 9), V.add(v1, v2, result) 204 | assert_equal_vector vec(1, 2, 3), v1 205 | assert_equal_vector vec(4, 5, 6), v2 206 | assert_equal_vector vec(5, 7, 9), result 207 | end 208 | 209 | def test_fun_sub() 210 | v1 = vec 9, 8, 7 211 | v2 = vec 1, 2, 3 212 | result = vec 213 | assert_equal_vector vec(8, 6, 4), V.sub(v1, v2, result) 214 | assert_equal_vector vec(9, 8, 7), v1 215 | assert_equal_vector vec(1, 2, 3), v2 216 | assert_equal_vector vec(8, 6, 4), result 217 | end 218 | 219 | def test_fun_mult() 220 | v1 = vec 1, 2, 3 221 | result = vec 222 | assert_equal_vector vec(2, 4, 6), V.mult(v1, 2, result) 223 | assert_equal_vector vec(1, 2, 3), v1 224 | assert_equal_vector vec(2, 4, 6), result 225 | end 226 | 227 | def test_fun_div() 228 | v1 = vec 2, 4, 6 229 | result = vec 230 | assert_equal_vector vec(1, 2, 3), V.div(v1, 2, result) 231 | assert_equal_vector vec(2, 4, 6), v1 232 | assert_equal_vector vec(1, 2, 3), result 233 | end 234 | 235 | def test_mag() 236 | assert_in_delta M.sqrt(5), vec(1, 2) .mag, 0.000001 237 | assert_in_delta M.sqrt(14), vec(1, 2, 3).mag, 0.000001 238 | end 239 | 240 | def test_magSq() 241 | assert_equal 5, vec(1, 2) .magSq 242 | assert_equal 14, vec(1, 2, 3).magSq 243 | end 244 | 245 | def test_setMag() 246 | v = vec 3, 4, 0 247 | assert_equal_vector vec(6, 8, 0), v.setMag(10) 248 | assert_equal_vector vec(6, 8, 0), v 249 | 250 | v = vec 3, 4, 0 251 | result = vec 252 | assert_equal_vector vec(6, 8, 0), v.setMag(result, 10) 253 | assert_equal_vector vec(3, 4, 0), v 254 | assert_equal_vector vec(6, 8, 0), result 255 | end 256 | 257 | def test_normalize() 258 | v = vec 1, 2, 3 259 | normal = v / v.mag 260 | assert_equal_vector normal, v.normalize 261 | assert_equal_vector normal, v 262 | 263 | v = vec 1, 2, 3 264 | result = vec 265 | assert_equal_vector normal, v.normalize(result) 266 | assert_equal_vector vec(1, 2, 3), v 267 | assert_equal_vector normal, result 268 | end 269 | 270 | def test_limit() 271 | v = vec 1, 2, 3 272 | assert_in_delta 1, v.limit(1).mag, 0.000001 273 | assert_in_delta 1, v .mag, 0.000001 274 | 275 | assert_in_delta 1, vec(1, 2, 3).limit(1).mag, 0.000001 276 | assert_in_delta 2, vec(1, 2, 3).limit(2).mag, 0.000001 277 | assert_in_delta 3, vec(1, 2, 3).limit(3).mag, 0.000001 278 | assert_in_delta vec(1, 2, 3).mag, vec(1, 2, 3).limit(4).mag, 0.000001 279 | end 280 | 281 | def test_dist() 282 | v1 = vec 1, 2, 3 283 | v2 = vec 4, 5, 6 284 | 285 | assert_in_delta M.sqrt((4-1)**2 + (5-2)**2 + (6-3)**2), v1.dist(v2), 0.000001 286 | assert_equal_vector vec(1, 2, 3), v1 287 | assert_equal_vector vec(4, 5, 6), v2 288 | 289 | assert_in_delta M.sqrt((4-1)**2 + (5-2)**2 + (6-3)**2), V.dist(v1, v2), 0.000001 290 | assert_equal_vector vec(1, 2, 3), v1 291 | assert_equal_vector vec(4, 5, 6), v2 292 | end 293 | 294 | def test_dot() 295 | v1 = vec 1, 2, 3 296 | v2 = vec 4, 5, 6 297 | 298 | assert_equal 1*4 + 2*5 + 3*6, v1.dot(4, 5, 6) 299 | assert_equal_vector vec(1, 2, 3), v1 300 | 301 | assert_equal 1*4 + 2*5 + 3*6, v1.dot(v2) 302 | assert_equal_vector vec(1, 2, 3), v1 303 | assert_equal_vector vec(4, 5, 6), v2 304 | 305 | assert_equal 1*4 + 2*5 + 3*6, V.dot(v1, v2) 306 | assert_equal_vector vec(1, 2, 3), v1 307 | assert_equal_vector vec(4, 5, 6), v2 308 | end 309 | 310 | def test_cross() 311 | v1 = vec 1, 0, 0 312 | v2 = vec 0, 1, 0 313 | 314 | assert_equal_vector vec(0, 0, 1), v1.cross(0, 1, 0) 315 | assert_equal_vector vec(1, 0, 0), v1 316 | 317 | result = vec 1, 2, 3 318 | assert_equal_vector vec(0, 0, 1), v1.cross(v2, result) 319 | assert_equal_vector vec(1, 0, 0), v1 320 | assert_equal_vector vec(0, 1, 0), v2 321 | assert_equal_vector vec(0, 0, 1), result 322 | 323 | result = vec 1, 2, 3 324 | assert_equal_vector vec(0, 0, 1), V.cross(v1, v2, result) 325 | assert_equal_vector vec(1, 0, 0), v1 326 | assert_equal_vector vec(0, 1, 0), v2 327 | assert_equal_vector vec(0, 0, 1), result 328 | end 329 | 330 | def test_rotate() 331 | angle = PI * 2 * 0.1 332 | context = Object.new.tap {|o| 333 | def o.toDegrees__(a) 334 | a * 2 * P::GraphicsContext::RAD2DEG__ 335 | end 336 | } 337 | 338 | v = vec 1, 0, 0 339 | assert_equal_vector vec(M.cos(angle), M.sin(angle), 0), v.rotate(angle) 340 | assert_equal_vector vec(M.cos(angle), M.sin(angle), 0), v 341 | 342 | v = vec 1, 0, 0, context: context 343 | assert_equal_vector vec(M.cos(angle * 2), M.sin(angle * 2), 0), v.rotate(angle) 344 | end 345 | 346 | def test_compare() 347 | v = vec 1, 2, 3 348 | assert v == vec(1, 2, 3) 349 | assert_not v != vec(1, 2, 3) 350 | 351 | assert v < vec(2, 2, 3) 352 | assert v < vec(1, 3, 3) 353 | assert v < vec(1, 2, 4) 354 | 355 | assert v > vec(0, 2, 3) 356 | assert v > vec(1, 1, 3) 357 | assert v > vec(1, 2, 2) 358 | 359 | assert_not v == nil 360 | assert v != nil 361 | assert_raise {v < nil} 362 | assert_raise {v > nil} 363 | end 364 | 365 | def test_fromAngle() 366 | angle = PI * 2 * 0.1 367 | result = vec 368 | assert_equal_vector vec(M.cos(angle), M.sin(angle), 0), V.fromAngle(angle) 369 | assert_equal_vector vec(M.cos(angle), M.sin(angle), 0), V.fromAngle(angle, result) 370 | assert_equal_vector vec(M.cos(angle), M.sin(angle), 0), result 371 | end 372 | 373 | def test_heading() 374 | angle = PI * 1 * 0.1 375 | assert_in_delta( angle, V.fromAngle( angle).heading, 0.000001) 376 | assert_in_delta(-angle, V.fromAngle(-angle).heading, 0.000001) 377 | end 378 | 379 | def test_angleBetween() 380 | v1 = V.fromAngle PI * 0.25 381 | v2 = V.fromAngle PI * 0.75 382 | assert_in_delta PI / 2, V.angleBetween(v1, v2), 0.000001 383 | end 384 | 385 | def test_lerp() 386 | assert_equal_vector vec(0.5, 0.5, 0.5), vec(0, 0, 0).lerp(vec(1, 1, 1), 0.5) 387 | assert_equal_vector vec(0.5, 0.5, 0.5), vec(0, 0, 0).lerp( 1, 1, 1, 0.5) 388 | assert_equal_vector vec(0.5, 0.5, 0.5), V.lerp(vec(0, 0, 0), vec(1, 1, 1), 0.5) 389 | end 390 | 391 | def test_random2D() 392 | v1 = V.random2D 393 | v2 = V.random2D 394 | assert v1.x != 0 395 | assert v1.y != 0 396 | assert_equal 0, v1.z 397 | assert v2.x != 0 398 | assert v2.y != 0 399 | assert_equal 0, v2.z 400 | assert_not_equal v1, v2 401 | assert_in_delta 1, v1.mag, 0.000001 402 | assert_in_delta 1, v2.mag, 0.000001 403 | end 404 | 405 | def test_random3D() 406 | v1 = V.random3D 407 | v2 = V.random3D 408 | assert v1.x != 0 409 | assert v1.y != 0 410 | assert v1.z != 0 411 | assert v2.x != 0 412 | assert v2.y != 0 413 | assert v2.z != 0 414 | assert_not_equal v1, v2 415 | assert_in_delta 1, v1.mag, 0.000001 416 | assert_in_delta 1, v2.mag, 0.000001 417 | end 418 | 419 | def test_inspect() 420 | assert_equal '#', vec(1, 2, 3).inspect 421 | end 422 | 423 | end# TestVector 424 | --------------------------------------------------------------------------------