Module: PSD::Compose

Extended by:
Compose
Included in:
Compose
Defined in:
lib/psd/compose.rb

Overview

Collection of methods that composes two RGBA pixels together in various ways based on Photoshop blend modes.

Mostly based on similar code from libpsd.

Constant Summary collapse

DEFAULT_OPTS =
{
  opacity: 255,
  fill_opacity: 255
}

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

If the blend mode is missing, we fall back to normal composition.



327
328
329
330
# File 'lib/psd/compose.rb', line 327

def method_missing(method, *args, &block)
  return ChunkyPNG::Color.send(method, *args) if ChunkyPNG::Color.respond_to?(method)
  normal(*args)
end

Instance Method Details

#color_burn(fg, bg, opts = {}) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/psd/compose.rb', line 59

def color_burn(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  calculate_foreground = Proc.new do |b, f|
    if f > 0
      f = ((255 - b) << 8) / f
      f > 255 ? 0 : (255 - f)
    else
      b
    end
  end

  new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
  new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
  new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#color_dodge(fg, bg, opts = {}) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/psd/compose.rb', line 124

def color_dodge(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  calculate_foreground = Proc.new do |b, f|
    f < 255 ? [(b << 8) / (255 - f), 255].min : b
  end

  new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
  new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
  new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#darken(fg, bg, opts = {}) ⇒ Object

Subtractive blend modes



35
36
37
38
39
40
41
42
43
44
45
# File 'lib/psd/compose.rb', line 35

def darken(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
  new_r = r(fg) <= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
  new_g = g(fg) <= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
  new_b = b(fg) <= b(bg) ? blend_channel(b(bg), b(fg), mix_alpha) : b(bg)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#difference(fg, bg, opts = {}) ⇒ Object

Inversion blend modes



300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/psd/compose.rb', line 300

def difference(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  new_r = blend_channel(r(bg), (r(bg) - r(fg)).abs, mix_alpha)
  new_g = blend_channel(g(bg), (g(bg) - g(fg)).abs, mix_alpha)
  new_b = blend_channel(b(bg), (b(bg) - b(fg)).abs, mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#exclusion(fg, bg, opts = {}) ⇒ Object



313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/psd/compose.rb', line 313

def exclusion(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  new_r = blend_channel(r(bg), r(bg) + r(fg) - (r(bg) * r(fg) >> 7), mix_alpha)
  new_g = blend_channel(g(bg), g(bg) + g(fg) - (g(bg) * g(fg) >> 7), mix_alpha)
  new_b = blend_channel(b(bg), b(bg) + b(fg) - (b(bg) * b(fg) >> 7), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#hard_light(fg, bg, opts = {}) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/psd/compose.rb', line 199

def hard_light(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  calculate_foreground = Proc.new do |b, f|
    if f < 128
      b * f >> 7
    else
      255 - ((255 - f) * (255 - b) >> 7)
    end
  end

  new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
  new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
  new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#hard_mix(fg, bg, opts = {}) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/psd/compose.rb', line 283

def hard_mix(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  new_r = blend_channel(r(bg), (r(bg) + r(fg) <= 255) ? 0 : 255, mix_alpha)
  new_g = blend_channel(g(bg), (g(bg) + g(fg) <= 255) ? 0 : 255, mix_alpha)
  new_b = blend_channel(b(bg), (b(bg) + b(fg) <= 255) ? 0 : 255, mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#lighten(fg, bg, opts = {}) ⇒ Object

Additive blend modes



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/psd/compose.rb', line 98

def lighten(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  new_r = r(fg) >= r(bg) ? blend_channel(r(bg), r(fg), mix_alpha) : r(bg)
  new_g = g(fg) >= g(bg) ? blend_channel(g(bg), g(fg), mix_alpha) : g(bg)
  new_b = b(fg) >= b(bg) ? blend_channel(b(bg), b(fg), mix_alpha) : b(bg)
  
  rgba(new_r, new_g, new_b, dst_alpha)
end

#linear_burn(fg, bg, opts = {}) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/psd/compose.rb', line 81

def linear_burn(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  new_r = blend_channel(r(bg), (r(fg) < (255 - r(bg))) ? 0 : r(fg) - (255 - r(bg)), mix_alpha)
  new_g = blend_channel(g(bg), (g(fg) < (255 - g(bg))) ? 0 : g(fg) - (255 - g(bg)), mix_alpha)
  new_b = blend_channel(b(bg), (b(fg) < (255 - b(bg))) ? 0 : b(fg) - (255 - b(bg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#linear_dodge(fg, bg, opts = {}) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/psd/compose.rb', line 141

def linear_dodge(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  new_r = blend_channel(r(bg), (r(bg) + r(fg)) > 255 ? 255 : r(bg) + r(fg), mix_alpha)
  new_g = blend_channel(g(bg), (g(bg) + g(fg)) > 255 ? 255 : g(bg) + g(fg), mix_alpha)
  new_b = blend_channel(b(bg), (b(bg) + b(fg)) > 255 ? 255 : b(bg) + b(fg), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#linear_light(fg, bg, opts = {}) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/psd/compose.rb', line 241

def linear_light(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  calculate_foreground = Proc.new do |b, f|
    if b < 255
      [f * f / (255 - b), 255].min
    else
      255
    end
  end

  new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
  new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
  new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#multiply(fg, bg, opts = {}) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
# File 'lib/psd/compose.rb', line 47

def multiply(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
  new_r = blend_channel(r(bg), r(fg) * r(bg) >> 8, mix_alpha)
  new_g = blend_channel(g(bg), g(fg) * g(bg) >> 8, mix_alpha)
  new_b = blend_channel(b(bg), b(fg) * b(bg) >> 8, mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#normal(fg, bg, opts = {}) ⇒ Object

Normal composition, delegate to ChunkyPNG



19
20
21
22
23
24
25
26
27
28
29
# File 'lib/psd/compose.rb', line 19

def normal(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))
  new_r = blend_channel(r(bg), r(fg), mix_alpha)
  new_g = blend_channel(g(bg), g(fg), mix_alpha)
  new_b = blend_channel(b(bg), b(fg), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#overlay(fg, bg, opts = {}) ⇒ Object

Contrasting blend modes



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/psd/compose.rb', line 159

def overlay(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  calculate_foreground = Proc.new do |b, f|
    if b < 128
      b * f >> 7
    else
      255 - ((255 - b) * (255 - f) >> 7)
    end
  end

  new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
  new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
  new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#pin_light(fg, bg, opts = {}) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/psd/compose.rb', line 262

def pin_light(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  calculate_foreground = Proc.new do |b, f|
    if f >= 128
      [b, (f - 128) * 2].max
    else
      [b, f * 2].min
    end
  end

  new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
  new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
  new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#screen(fg, bg, opts = {}) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/psd/compose.rb', line 111

def screen(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  new_r = blend_channel(r(bg), 255 - ((255 - r(bg)) * (255 - r(fg)) >> 8), mix_alpha)
  new_g = blend_channel(g(bg), 255 - ((255 - g(bg)) * (255 - g(fg)) >> 8), mix_alpha)
  new_b = blend_channel(b(bg), 255 - ((255 - b(bg)) * (255 - b(fg)) >> 8), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#soft_light(fg, bg, opts = {}) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/psd/compose.rb', line 180

def soft_light(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  calculate_foreground = Proc.new do |b, f|
    c1 = b * f >> 8
    c2 = 255 - ((255 - b) * (255 - f) >> 8)
    ((255 - b) * c1 >> 8) + (b * c2 >> 8)
  end

  new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
  new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
  new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end

#vivid_light(fg, bg, opts = {}) ⇒ Object



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/psd/compose.rb', line 220

def vivid_light(fg, bg, opts={})
  return fg if fully_transparent?(bg)
  return bg if fully_transparent?(fg)

  mix_alpha, dst_alpha = calculate_alphas(fg, bg, DEFAULT_OPTS.merge(opts))

  calculate_foreground = Proc.new do |b, f|
    if f < 255
      [(b * b / (255 - f) + f * f / (255 - b)) >> 1, 255].min
    else
      b
    end
  end

  new_r = blend_channel(r(bg), calculate_foreground.call(r(bg), r(fg)), mix_alpha)
  new_g = blend_channel(g(bg), calculate_foreground.call(g(bg), g(fg)), mix_alpha)
  new_b = blend_channel(b(bg), calculate_foreground.call(b(bg), b(fg)), mix_alpha)

  rgba(new_r, new_g, new_b, dst_alpha)
end