Skip to content

Instantly share code, notes, and snippets.

@spinnylights
Created March 11, 2022 23:23
Show Gist options
  • Save spinnylights/b8fce0f82aafd858dd45fbe4656a0ab1 to your computer and use it in GitHub Desktop.
Save spinnylights/b8fce0f82aafd858dd45fbe4656a0ab1 to your computer and use it in GitHub Desktop.
GLSL-style component swizzling mixin in Ruby
##
# Module Swizzleable allows for [GLSL-style component
# swizzling](https://www.khronos.org/opengl/wiki/Data_Type_(GLSL)#Swizzling)
# on instances of the including class, e.g. `vec.wwyx`,
# `color.rgb`, etc.
#
# ```ruby
# class Vec4
# include Swizzleable
#
# def initialize(x, y, z, w)
# @vals = [x, y, z, w]
# end
#
# def swizz_vals
# @vals
# end
# end
#
# v = Vec4.new(1, 2, 3, 4)
#
# v.x
# #=> [1]
# v.zzz
# #=> [3, 3, 3]
# v.raga
# #=> [1, 4, 2, 4]
# v.ps
# #=> [3, 1]
#
# v.wzzy
# #=> [4, 3, 3, 2]
# v.zy = [:meow, :cow]
# v.wzzy
# #=> [4, :meow, :meow, :cow]
# ```
#
# To use, define `YourClass#swizz_vals`. This should return an
# instance variable containing an array with entries
# corresponding to each component in order, e.g. `[0.5, 0.7, 0.3,
# 0.2]` for `r => 0.5`, `g => 0.7`, etc.
#
# Both getters and setters will be defined. As in GLSL, setters
# are not defined for masks with repeated swizzle components.
#
# ```ruby
# v.zyxw = [:meow, :cow].cycle.lazy
# v.xzzy
# #=> [:meow, :meow, :meow, :cow]
# v.xxyy = [:meow, :cow].cycle.lazy
# #=> NoMethodError: undefined method `xxyy=`
# ```
#
# It's important that `YourClass#swizz_vals` return an instance
# variable and not a fresh array in order for the setters to
# work, as they need to be able to access the underlying values
# within the instance.
#
# By default, Swizzleable provides the same set of swizzle masks
# available in GLSL: `xyzw`, `rgba`, and `stpq`. If you
# want a different set of swizzle masks, define
# `YourClass::swizz_masks` before including Swizzleable.
#
# ```ruby
# class Rect
# def self.swizz_masks
# [[:w, :h]]
# end
#
# include Swizzleable
#
# def initialize(w, h)
# @dims = [w, h]
# end
#
# def swizz_vals
# @dims
# end
# end
#
# r = Rect.new(100, 200)
#
# r.hw
# #=> [200, 100]
# r.hhww
# #=> [200, 200, 100, 100]
# r.xyz
# #=> NoMethodError: undefined method `xyz'
# ```
#
# `YourClass::swizz_masks` should be an array of arrays of
# symbols, with each symbol corresponding to an entry of
# `YourClass#swizz_vals` in order. The maximum length of a
# swizzle mask for `YourClass` will then be either the length of
# the longest array in `YourClass::swizz_masks` or `4`, whichever
# is larger. If this ends up being larger than the length of
# `YourClass#swizz_vals`, swizzle components that refer to
# entries past the end of `YourClass#swizz_vals` will be filled
# in with `nil`.
#
# ```ruby
# class Vec2
# include Swizzleable
#
# def initialize(x, y)
# @entries = [x, y]
# end
#
# def swizz_vals
# @entries
# end
# end
#
# Vec2.new(1, 2).xyzw
# #=> [1, 2, nil, nil]
# ```
module Swizzleable
module ClassMethods
def swizz_masks
[
[:r, :g, :b, :a],
[:x, :y, :z, :w],
[:s, :t, :p, :q],
]
end
end
def self.included(klass)
klass.extend(ClassMethods)
min_max_swizz_len = 4
max_swizz_len = klass.swizz_masks.map(&:length).max
max_swizz_len = min_max_swizz_len if max_swizz_len < min_max_swizz_len
klass.swizz_masks.each do |comp_set|
1.upto(max_swizz_len) do |n|
comp_set.repeated_permutation(n).each do |comps|
klass.define_method(comps.join.to_sym) do
comp_set.zip(swizz_vals).to_h.fetch_values(*comps)
end
end
comp_set.permutation(n).each do |comps|
klass.define_method((comps.join + '=').to_sym) do |args|
comps.map(&comp_set.method(:index)).zip(args).each do |ndx, arg|
swizz_vals[ndx] = arg
end
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment