1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <stdio.h>
6#include <stdlib.h>
7
8#include "ppapi/c/ppb_image_data.h"
9#include "ppapi/cpp/graphics_2d.h"
10#include "ppapi/cpp/image_data.h"
11#include "ppapi/cpp/input_event.h"
12#include "ppapi/cpp/instance.h"
13#include "ppapi/cpp/module.h"
14#include "ppapi/cpp/point.h"
15#include "ppapi/utility/completion_callback_factory.h"
16
17#ifdef WIN32
18#undef PostMessage
19// Allow 'this' in initializer list
20#pragma warning(disable : 4355)
21#endif
22
23namespace {
24
25static const int kMouseRadius = 20;
26
27uint8_t RandUint8(uint8_t min, uint8_t max) {
28  uint64_t r = rand();
29  uint8_t result = static_cast<uint8_t>(r * (max - min + 1) / RAND_MAX) + min;
30  return result;
31}
32
33uint32_t MakeColor(uint8_t r, uint8_t g, uint8_t b) {
34  uint8_t a = 255;
35  PP_ImageDataFormat format = pp::ImageData::GetNativeImageDataFormat();
36  if (format == PP_IMAGEDATAFORMAT_BGRA_PREMUL) {
37    return (a << 24) | (r << 16) | (g << 8) | b;
38  } else {
39    return (a << 24) | (b << 16) | (g << 8) | r;
40  }
41}
42
43}  // namespace
44
45class Graphics2DInstance : public pp::Instance {
46 public:
47  explicit Graphics2DInstance(PP_Instance instance)
48      : pp::Instance(instance),
49        callback_factory_(this),
50        mouse_down_(false),
51        buffer_(NULL),
52        device_scale_(1.0f) {}
53
54  ~Graphics2DInstance() { delete[] buffer_; }
55
56  virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]) {
57    RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE);
58
59    unsigned int seed = 1;
60    srand(seed);
61    CreatePalette();
62    return true;
63  }
64
65  virtual void DidChangeView(const pp::View& view) {
66    device_scale_ = view.GetDeviceScale();
67    pp::Size new_size = pp::Size(view.GetRect().width() * device_scale_,
68                                 view.GetRect().height() * device_scale_);
69
70    if (!CreateContext(new_size))
71      return;
72
73    // When flush_context_ is null, it means there is no Flush callback in
74    // flight. This may have happened if the context was not created
75    // successfully, or if this is the first call to DidChangeView (when the
76    // module first starts). In either case, start the main loop.
77    if (flush_context_.is_null())
78      MainLoop(0);
79  }
80
81  virtual bool HandleInputEvent(const pp::InputEvent& event) {
82    if (!buffer_)
83      return true;
84
85    if (event.GetType() == PP_INPUTEVENT_TYPE_MOUSEDOWN ||
86        event.GetType() == PP_INPUTEVENT_TYPE_MOUSEMOVE) {
87      pp::MouseInputEvent mouse_event(event);
88
89      if (mouse_event.GetButton() == PP_INPUTEVENT_MOUSEBUTTON_NONE)
90        return true;
91
92      mouse_ = pp::Point(mouse_event.GetPosition().x() * device_scale_,
93                         mouse_event.GetPosition().y() * device_scale_);
94      mouse_down_ = true;
95    }
96
97    if (event.GetType() == PP_INPUTEVENT_TYPE_MOUSEUP)
98      mouse_down_ = false;
99
100    return true;
101  }
102
103 private:
104  void CreatePalette() {
105    for (int i = 0; i < 64; ++i) {
106      // Black -> Red
107      palette_[i] = MakeColor(i * 2, 0, 0);
108      palette_[i + 64] = MakeColor(128 + i * 2, 0, 0);
109      // Red -> Yellow
110      palette_[i + 128] = MakeColor(255, i * 4, 0);
111      // Yellow -> White
112      palette_[i + 192] = MakeColor(255, 255, i * 4);
113    }
114  }
115
116  bool CreateContext(const pp::Size& new_size) {
117    const bool kIsAlwaysOpaque = true;
118    context_ = pp::Graphics2D(this, new_size, kIsAlwaysOpaque);
119    // Call SetScale before BindGraphics so the image is scaled correctly on
120    // HiDPI displays.
121    context_.SetScale(1.0f / device_scale_);
122    if (!BindGraphics(context_)) {
123      fprintf(stderr, "Unable to bind 2d context!\n");
124      context_ = pp::Graphics2D();
125      return false;
126    }
127
128    // Allocate a buffer of palette entries of the same size as the new context.
129    buffer_ = new uint8_t[new_size.width() * new_size.height()];
130    size_ = new_size;
131
132    return true;
133  }
134
135  void Update() {
136    // Old-school fire technique cribbed from
137    // http://ionicsolutions.net/2011/12/30/demo-fire-effect/
138    UpdateCoals();
139    DrawMouse();
140    UpdateFlames();
141  }
142
143  void UpdateCoals() {
144    int width = size_.width();
145    int height = size_.height();
146    size_t span = 0;
147
148    // Draw two rows of random values at the bottom.
149    for (int y = height - 2; y < height; ++y) {
150      size_t offset = y * width;
151      for (int x = 0; x < width; ++x) {
152        // On a random chance, draw some longer strips of brighter colors.
153        if (span || RandUint8(1, 4) == 1) {
154          if (!span)
155            span = RandUint8(10, 20);
156          buffer_[offset + x] = RandUint8(128, 255);
157          span--;
158        } else {
159          buffer_[offset + x] = RandUint8(32, 96);
160        }
161      }
162    }
163  }
164
165  void UpdateFlames() {
166    int width = size_.width();
167    int height = size_.height();
168    for (int y = 1; y < height - 1; ++y) {
169      size_t offset = y * width;
170      for (int x = 1; x < width - 1; ++x) {
171        int sum = 0;
172        sum += buffer_[offset - width + x - 1];
173        sum += buffer_[offset - width + x + 1];
174        sum += buffer_[offset + x - 1];
175        sum += buffer_[offset + x + 1];
176        sum += buffer_[offset + width + x - 1];
177        sum += buffer_[offset + width + x];
178        sum += buffer_[offset + width + x + 1];
179        buffer_[offset - width + x] = sum / 7;
180      }
181    }
182  }
183
184  void DrawMouse() {
185    if (!mouse_down_)
186      return;
187
188    int width = size_.width();
189    int height = size_.height();
190
191    // Draw a circle at the mouse position.
192    int radius = kMouseRadius * device_scale_;
193    int cx = mouse_.x();
194    int cy = mouse_.y();
195    int minx = cx - radius <= 0 ? 1 : cx - radius;
196    int maxx = cx + radius >= width ? width - 1 : cx + radius;
197    int miny = cy - radius <= 0 ? 1 : cy - radius;
198    int maxy = cy + radius >= height ? height - 1 : cy + radius;
199    for (int y = miny; y < maxy; ++y) {
200      for (int x = minx; x < maxx; ++x) {
201        if ((x - cx) * (x - cx) + (y - cy) * (y - cy) < radius * radius)
202          buffer_[y * width + x] = RandUint8(192, 255);
203      }
204    }
205  }
206
207  void Paint() {
208    // See the comment above the call to ReplaceContents below.
209    PP_ImageDataFormat format = pp::ImageData::GetNativeImageDataFormat();
210    const bool kDontInitToZero = false;
211    pp::ImageData image_data(this, format, size_, kDontInitToZero);
212
213    uint32_t* data = static_cast<uint32_t*>(image_data.data());
214    if (!data)
215      return;
216
217    uint32_t num_pixels = size_.width() * size_.height();
218    size_t offset = 0;
219    for (uint32_t i = 0; i < num_pixels; ++i) {
220      data[offset] = palette_[buffer_[offset]];
221      offset++;
222    }
223
224    // Using Graphics2D::ReplaceContents is the fastest way to update the
225    // entire canvas every frame. According to the documentation:
226    //
227    //   Normally, calling PaintImageData() requires that the browser copy
228    //   the pixels out of the image and into the graphics context's backing
229    //   store. This function replaces the graphics context's backing store
230    //   with the given image, avoiding the copy.
231    //
232    //   In the case of an animation, you will want to allocate a new image for
233    //   the next frame. It is best if you wait until the flush callback has
234    //   executed before allocating this bitmap. This gives the browser the
235    //   option of caching the previous backing store and handing it back to
236    //   you (assuming the sizes match). In the optimal case, this means no
237    //   bitmaps are allocated during the animation, and the backing store and
238    //   "front buffer" (which the module is painting into) are just being
239    //   swapped back and forth.
240    //
241    context_.ReplaceContents(&image_data);
242  }
243
244  void MainLoop(int32_t) {
245    if (context_.is_null()) {
246      // The current Graphics2D context is null, so updating and rendering is
247      // pointless. Set flush_context_ to null as well, so if we get another
248      // DidChangeView call, the main loop is started again.
249      flush_context_ = context_;
250      return;
251    }
252
253    Update();
254    Paint();
255    // Store a reference to the context that is being flushed; this ensures
256    // the callback is called, even if context_ changes before the flush
257    // completes.
258    flush_context_ = context_;
259    context_.Flush(
260        callback_factory_.NewCallback(&Graphics2DInstance::MainLoop));
261  }
262
263  pp::CompletionCallbackFactory<Graphics2DInstance> callback_factory_;
264  pp::Graphics2D context_;
265  pp::Graphics2D flush_context_;
266  pp::Size size_;
267  pp::Point mouse_;
268  bool mouse_down_;
269  uint8_t* buffer_;
270  uint32_t palette_[256];
271  float device_scale_;
272};
273
274class Graphics2DModule : public pp::Module {
275 public:
276  Graphics2DModule() : pp::Module() {}
277  virtual ~Graphics2DModule() {}
278
279  virtual pp::Instance* CreateInstance(PP_Instance instance) {
280    return new Graphics2DInstance(instance);
281  }
282};
283
284namespace pp {
285Module* CreateModule() { return new Graphics2DModule(); }
286}  // namespace pp
287