[gtk: 1/2] gtk-demo: Add shadertoy demo




commit 0092a08dfcdf2cfd4db7664ebc330775bf631e5e
Author: Alexander Larsson <alexl redhat com>
Date:   Mon Sep 7 15:32:22 2020 +0200

    gtk-demo: Add shadertoy demo
    
    This adds a small demo of using OpenGL shaders, it renders a quad
    over the entire widget with a custom fragment shader. The coordinates
    and the uniform names are compatible with the ones on shadertoy.com
    (although some features, like texture inputs are missing currently).
    
    The default shader in the demo is
    https://www.shadertoy.com/view/wsjBD3 which is CC0, so it is
    redistributable by Gtk+ (most other shaders are CC-BY-NC-SA which
    isn't obviously compatible). I also added a set of buttons loading
    a few other CC0 shaders I found.

 demos/gtk-demo/alienplanet.glsl   | 345 +++++++++++++++++++++++++
 demos/gtk-demo/cogs.glsl          | 224 ++++++++++++++++
 demos/gtk-demo/demo.gresource.xml |  10 +
 demos/gtk-demo/glowingstars.glsl  | 174 +++++++++++++
 demos/gtk-demo/gtkshadertoy.c     | 524 ++++++++++++++++++++++++++++++++++++++
 demos/gtk-demo/gtkshadertoy.h     |  34 +++
 demos/gtk-demo/mandelbrot.glsl    |  95 +++++++
 demos/gtk-demo/meson.build        |   2 +
 demos/gtk-demo/neon.glsl          | 220 ++++++++++++++++
 demos/gtk-demo/shadertoy.c        | 191 ++++++++++++++
 10 files changed, 1819 insertions(+)
---
diff --git a/demos/gtk-demo/alienplanet.glsl b/demos/gtk-demo/alienplanet.glsl
new file mode 100644
index 0000000000..3e3aea3ac8
--- /dev/null
+++ b/demos/gtk-demo/alienplanet.glsl
@@ -0,0 +1,345 @@
+// Originally from: https://www.shadertoy.com/view/wsjBD3
+// License CC0: A battered alien planet
+//  Been experimenting with space inspired shaders
+
+#define PI  3.141592654
+#define TAU (2.0*PI)
+
+#define TOLERANCE       0.00001
+#define MAX_ITER        65
+#define MIN_DISTANCE    0.01
+#define MAX_DISTANCE    9.0
+
+const vec3  skyCol1       = vec3(0.35, 0.45, 0.6);
+const vec3  skyCol2       = vec3(0.4, 0.7, 1.0);
+const vec3  skyCol3       = pow(skyCol1, vec3(0.25));
+const vec3  sunCol1       = vec3(1.0,0.6,0.4);
+const vec3  sunCol2       = vec3(1.0,0.9,0.7);
+const vec3  smallSunCol1  = vec3(1.0,0.5,0.25)*0.5;
+const vec3  smallSunCol2  = vec3(1.0,0.5,0.25)*0.5;
+const vec3  mountainColor = 1.0*sqrt(vec3(0.95, 0.65, 0.45));
+const float cellWidth     = 1.0;
+const vec4  planet        = vec4(80.0, -20.0, 100.0, 50.0)*1000.0;
+
+void rot(inout vec2 p, float a) {
+  float c = cos(a);
+  float s = sin(a);
+  p = vec2(p.x*c + p.y*s, -p.x*s + p.y*c);
+}
+
+vec2 mod2(inout vec2 p, vec2 size) {
+  vec2 c = floor((p + size*0.5)/size);
+  p = mod(p + size*0.5,size) - size*0.5;
+  return c;
+}
+
+float circle(vec2 p, float r) {
+  return length(p) - r;
+}
+
+float egg(vec2 p, float ra, float rb) {
+  const float k = sqrt(3.0);
+  p.x = abs(p.x);
+  float r = ra - rb;
+  return ((p.y<0.0)       ? length(vec2(p.x,  p.y    )) - r :
+          (k*(p.x+r)<p.y) ? length(vec2(p.x,  p.y-k*r)) :
+                              length(vec2(p.x+r,p.y    )) - 2.0*r) - rb;
+}
+
+vec2 hash(vec2 p) {
+  p = vec2(dot (p, vec2 (127.1, 311.7)), dot (p, vec2 (269.5, 183.3)));
+  return -1. + 2.*fract (sin (p)*43758.5453123);
+}
+
+vec2 raySphere(vec3 ro, vec3 rd, vec4 sphere) {
+  vec3 center = sphere.xyz;
+  float radius = sphere.w;
+  vec3 m = ro - center.xyz;
+  float b = dot(m, rd);
+  float c = dot(m, m) - radius*radius;
+  if(c > 0.0 && b > 0.0) return vec2(-1.0, -1.0);
+  float discr = b * b - c;
+  if(discr < 0.0) return vec2(-1.0);
+  float normalMultiplier = 1.0;
+  float s = sqrt(discr);
+  float t0 = -b - s;
+  float t1 = -b + s;;
+  return vec2(t0, t1);
+}
+
+float noise1(vec2 p) {
+  vec2 n = mod2(p, vec2(cellWidth));
+  vec2 hh = hash(sqrt(2.0)*(n+1000.0));
+  hh.x *= hh.y;
+
+  float r = 0.225*cellWidth;
+
+  float d = circle(p, 2.0*r);
+
+  float h = hh.x*smoothstep(0.0, r, -d);
+
+  return h*0.25;
+}
+
+float noise2(vec2 p) {
+  vec2 n = mod2(p, vec2(cellWidth));
+  vec2 hh = hash(sqrt(2.0)*(n+1000.0));
+  hh.x *= hh.y;
+
+  rot(p, TAU*hh.y);
+  float r = 0.45*cellWidth;
+
+//  float d = circle(p, 1.0*r);
+  float d = egg(p, 0.75*r, 0.5*r*abs(hh.y));
+
+  float h = (hh.x)*smoothstep(0.0, r, -2.0*d);
+
+  return h*0.275;
+}
+
+
+float height(vec2 p, float dd, int mx) {
+  const float aa   = 0.45;
+  const float ff   = 2.03;
+  const float tt   = 1.2;
+  const float oo   = 3.93;
+  const float near = 0.25;
+  const float far  = 0.65;
+
+  float a = 1.0;
+  float o = 0.2;
+  float s = 0.0;
+  float d = 0.0;
+
+  int i = 0;
+
+  for (; i < 4;++i) {
+    float nn = a*noise2(p);
+    s += nn;
+    d += abs(a);
+    p += o;
+    a *= aa;
+    p *= ff;
+    o *= oo;
+    rot(p, tt);
+  }
+
+  float lod = s/d;
+
+  float rdd = dd/MAX_DISTANCE;
+  mx = int(mix(float(4), float(mx), step(rdd, far)));
+
+  for (; i < mx; ++i) {
+    float nn = a*noise1(p);
+    s += nn;
+    d += abs(a);
+    p += o;
+    a *= aa;
+    p *= ff;
+    o *= oo;
+    rot(p, tt);
+  }
+
+  float hid = (s/d);
+
+  return mix(hid, lod, smoothstep(near, far, rdd));
+}
+
+float loheight(vec2 p, float d) {
+  return height(p, d, 0);
+}
+
+float height(vec2 p, float d) {
+  return height(p, d, 6);
+}
+
+float hiheight(vec2 p, float d) {
+  return height(p, d, 8);
+}
+
+vec3 normal(vec2 p, float d) {
+  vec2 eps = vec2(0.00125, 0.0);
+
+  vec3 n;
+
+  n.x = (hiheight(p - eps.xy, d) - hiheight(p + eps.xy, d));
+  n.y = 2.0*eps.x;
+  n.z = (hiheight(p - eps.yx, d) - hiheight(p + eps.yx, d));
+
+  return normalize(n);
+}
+
+const float stepLength[] = float[](0.9, 0.25);
+
+
+float march(vec3 ro, vec3 rd, out int max_iter) {
+  float dt = 0.1;
+  float d = MIN_DISTANCE;
+  int currentStep = 0;
+  float lastd = d;
+  for (int i = 0; i < MAX_ITER; ++i)
+  {
+    vec3 p = ro + d*rd;
+    float h = height(p.xz, d);
+
+    if (d > MAX_DISTANCE) {
+      max_iter = i;
+      return MAX_DISTANCE;
+    }
+
+    float hd = p.y - h;
+
+    if (hd < TOLERANCE) {
+      ++currentStep;
+      if (currentStep >= stepLength.length()) {
+        max_iter = i;
+        return d;
+      }
+
+      d = lastd;
+      continue;
+    }
+
+    float sl = stepLength[currentStep];
+
+    dt = max(hd, TOLERANCE)*sl + 0.0025*d;
+    lastd = d;
+    d += dt;
+  }
+
+  max_iter = MAX_ITER;
+  return MAX_DISTANCE;
+}
+
+vec3 sunDirection() {
+  return normalize(vec3(-0.5, 0.085, 1.0));
+}
+
+vec3 smallSunDirection() {
+  return normalize(vec3(-0.2, -0.05, 1.0));
+}
+
+float psin(float f) {
+  return 0.5 + 0.5*sin(f);
+}
+
+vec3 skyColor(vec3 ro, vec3 rd) {
+  vec3 sunDir = sunDirection();
+  vec3 smallSunDir = smallSunDirection();
+
+  float sunDot = max(dot(rd, sunDir), 0.0);
+  float smallSunDot = max(dot(rd, smallSunDir), 0.0);
+
+  float angle = atan(rd.y, length(rd.xz))*2.0/PI;
+
+  vec3 skyCol = mix(mix(skyCol1, skyCol2, max(0.0, angle)), skyCol3, clamp(-angle*2.0, 0.0, 1.0));
+
+  vec3 sunCol = 0.5*sunCol1*pow(sunDot, 20.0) + 8.0*sunCol2*pow(sunDot, 2000.0);
+  vec3 smallSunCol = 0.5*smallSunCol1*pow(smallSunDot, 200.0) + 8.0*smallSunCol2*pow(smallSunDot, 20000.0);
+
+  vec3 dust = pow(sunCol2*mountainColor, vec3(1.75))*smoothstep(0.05, -0.1, rd.y)*0.5;
+
+  vec2 si = raySphere(ro, rd, planet);
+
+  vec3 planetSurface = ro + si.x*rd;
+  vec3 planetNormal = normalize(planetSurface - planet.xyz);
+  float planetDiff = max(dot(planetNormal, sunDir), 0.0);
+  float planetBorder = max(dot(planetNormal, -rd), 0.0);
+  float planetLat = (planetSurface.x+planetSurface.y)*0.0005;
+  vec3 planetCol = mix(1.3*vec3(0.9, 0.8, 0.7), 0.3*vec3(0.9, 0.8, 0.7), 
pow(psin(planetLat+1.0)*psin(sqrt(2.0)*planetLat+2.0)*psin(sqrt(3.5)*planetLat+3.0), 0.5));
+
+  vec3 final = vec3(0.0);
+
+  final += step(0.0, si.x)*pow(planetDiff, 0.75)*planetCol*smoothstep(-0.075, 0.0, rd.y)*smoothstep(0.0, 
0.1, planetBorder);
+
+  final += skyCol + sunCol + smallSunCol + dust;
+
+  return final;
+}
+
+vec3 getColor(vec3 ro, vec3 rd) {
+  int max_iter = 0;
+  vec3 skyCol = skyColor(ro, rd);
+  vec3 col = vec3(0);
+
+  float d = march(ro, rd, max_iter);
+
+  if (d < MAX_DISTANCE)   {
+    vec3 sunDir = sunDirection();
+    vec3 osunDir = sunDir*vec3(-1.0, .0, -1.0);
+    vec3 p = ro + d*rd;
+
+    vec3 normal = normal(p.xz, d);
+
+    float amb = 0.2;
+
+    float dif1 = max(0.0, dot(sunDir, normal));
+    vec3 shd1 = sunCol2*mix(amb, 1.0, pow(dif1, 0.75));
+
+    float dif2 = max(0.0, dot(osunDir, normal));
+    vec3 shd2 = sunCol1*mix(amb, 1.0, pow(dif2, 0.75));
+
+    vec3 ref = reflect(rd, normal);
+    vec3 rcol = skyColor(p, ref);
+
+    col = mountainColor*amb*skyCol3;
+    col += mix(shd1, shd2, -0.5)*mountainColor;
+    float fre = max(dot(normal, -rd), 0.0);
+    fre = pow(1.0 - fre, 5.0);
+    col += rcol*fre*0.5;
+    col += (1.0*p.y);
+    col = tanh(col);
+    col = mix(col, skyCol, smoothstep(0.5*MAX_DISTANCE, 1.0*MAX_DISTANCE, d));
+
+  } else {
+    col = skyCol;
+  }
+
+//  col += vec3(1.1, 0.0, 0.0)* smoothstep(0.25, 1.0,(float(max_iter)/float(MAX_ITER)));
+  return col;
+}
+
+vec3 getSample1(vec2 p, float time) {
+  float off = 0.5*iTime;
+
+  vec3 ro  = vec3(0.5, 1.0-0.25, -2.0 + off);
+  vec3 la  = ro + vec3(0.0, -0.30,  2.0);
+
+  vec3 ww = normalize(la - ro);
+  vec3 uu = normalize(cross(vec3(0.0,1.0,0.0), ww));
+  vec3 vv = normalize(cross(ww, uu));
+  vec3 rd = normalize(p.x*uu + p.y*vv + 2.0*ww);
+
+  vec3 col = getColor(ro, rd)  ;
+
+  return col;
+}
+
+vec3 getSample2(vec2 p, float time) {
+  p.y-=time*0.25;
+  float h = height(p, 0.0);
+  vec3 n = normal(p, 0.0);
+
+  vec3 lp = vec3(10.0, -1.2, 0.0);
+
+  vec3 ld = normalize(vec3(p.x, h, p.y)- lp);
+
+  float d = max(dot(ld, n), 0.0);
+
+  vec3 col = vec3(0.0);
+
+  col = vec3(1.0)*(h+0.1);
+  col += vec3(1.5)*pow(d, 0.75);
+
+  return col;
+}
+
+void mainImage(out vec4 fragColor, vec2 fragCoord) {
+  vec2 q = fragCoord.xy/iResolution.xy;
+  vec2 p = -1.0 + 2.0*q;
+  p.x *= iResolution.x/iResolution.y;
+
+  vec3 col = getSample1(p, iTime);
+
+  fragColor = vec4(col, 1.0);
+}
diff --git a/demos/gtk-demo/cogs.glsl b/demos/gtk-demo/cogs.glsl
new file mode 100644
index 0000000000..a0768bdb39
--- /dev/null
+++ b/demos/gtk-demo/cogs.glsl
@@ -0,0 +1,224 @@
+// Originally from: https://www.shadertoy.com/view/3ljyDD
+// License CC0: Hexagonal tiling + cog wheels
+//  Nothing fancy, just hexagonal tiling + cog wheels
+
+#define PI      3.141592654
+#define TAU     (2.0*PI)
+#define MROT(a) mat2(cos(a), sin(a), -sin(a), cos(a))
+
+float hash(in vec2 co) {
+  return fract(sin(dot(co.xy ,vec2(12.9898,58.233))) * 13758.5453);
+}
+
+float pcos(float a) {
+  return 0.5 + 0.5*cos(a);
+}
+
+void rot(inout vec2 p, float a) {
+  float c = cos(a);
+  float s = sin(a);
+  p = vec2(c*p.x + s*p.y, -s*p.x + c*p.y);
+}
+
+float modPolar(inout vec2 p, float repetitions) {
+  float angle = 2.0*PI/repetitions;
+  float a = atan(p.y, p.x) + angle/2.;
+  float r = length(p);
+  float c = floor(a/angle);
+  a = mod(a,angle) - angle/2.;
+  p = vec2(cos(a), sin(a))*r;
+  // For an odd number of repetitions, fix cell index of the cell in -x direction
+  // (cell index would be e.g. -5 and 5 in the two halves of the cell):
+  if (abs(c) >= (repetitions/2.0)) c = abs(c);
+  return c;
+}
+
+float pmin(float a, float b, float k) {
+  float h = clamp( 0.5+0.5*(b-a)/k, 0.0, 1.0 );
+  return mix( b, a, h ) - k*h*(1.0-h);
+}
+
+const vec2 sz       = vec2(1.0, sqrt(3.0));
+const vec2 hsz      = 0.5*sz;
+const float smallCount = 16.0;
+
+vec2 hextile(inout vec2 p) {
+  // See Art of Code: Hexagonal Tiling Explained!
+  // https://www.youtube.com/watch?v=VmrIDyYiJBA
+
+  vec2 p1 = mod(p, sz)-hsz;
+  vec2 p2 = mod(p - hsz*1.0, sz)-hsz;
+  vec2 p3 = mix(p2, p1, vec2(length(p1) < length(p2)));
+  vec2 n = p3 - p;
+  p = p3;
+
+  return n;
+}
+
+float circle(vec2 p, float r) {
+  return length(p) - r;
+}
+
+float box(vec2 p, vec2 b) {
+  vec2 d = abs(p)-b;
+  return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
+}
+
+float unevenCapsule(vec2 p, float r1, float r2, float h) {
+  p.x = abs(p.x);
+  float b = (r1-r2)/h;
+  float a = sqrt(1.0-b*b);
+  float k = dot(p,vec2(-b,a));
+  if( k < 0.0 ) return length(p) - r1;
+  if( k > a*h ) return length(p-vec2(0.0,h)) - r2;
+  return dot(p, vec2(a,b) ) - r1;
+}
+
+float cogwheel(vec2 p, float innerRadius, float outerRadius, float cogs, float holes) {
+  float cogWidth  = 0.25*innerRadius*TAU/cogs;
+
+  float d0 = circle(p, innerRadius);
+
+  vec2 icp = p;
+  modPolar(icp, holes);
+  icp -= vec2(innerRadius*0.55, 0.0);
+  float d1 = circle(icp, innerRadius*0.25);
+
+  vec2 cp = p;
+  modPolar(cp, cogs);
+  cp -= vec2(innerRadius, 0.0);
+  float d2 = unevenCapsule(cp.yx, cogWidth, cogWidth*0.75, (outerRadius-innerRadius));
+
+  float d3 = circle(p, innerRadius*0.20);
+
+  float d = 1E6;
+  d = min(d, d0);
+  d = pmin(d, d2, 0.5*cogWidth);
+  d = min(d, d2);
+  d = max(d, -d1);
+  d = max(d, -d3);
+
+  return d;
+}
+
+float ccell1(vec2 p, float r) {
+  float d = 1E6;
+  const float bigCount = 60.0;
+
+  vec2 cp0 = p;
+  rot(cp0, -iTime*TAU/bigCount);
+  float d0 = cogwheel(cp0, 0.36, 0.38, bigCount, 5.0);
+
+  vec2 cp1 = p;
+  float nm = modPolar(cp1, 6.0);
+
+  cp1 -= vec2(0.5, 0.0);
+  rot(cp1, 0.2+TAU*nm/2.0 + iTime*TAU/smallCount);
+  float d1 = cogwheel(cp1, 0.11, 0.125, smallCount, 5.0);
+
+  d = min(d, d0);
+  d = min(d, d1);
+  return d;
+}
+
+float ccell2(vec2 p, float r) {
+  float d = 1E6;
+  vec2 cp0 = p;
+  float nm = modPolar(cp0, 6.0);
+  vec2 cp1 = cp0;
+  const float off = 0.275;
+  const float count = smallCount + 2.0;
+  cp0 -= vec2(off, 0.0);
+  rot(cp0, 0.+TAU*nm/2.0 - iTime*TAU/count);
+  float d0 = cogwheel(cp0, 0.09, 0.105, count, 5.0);
+
+
+  cp1 -= vec2(0.5, 0.0);
+  rot(cp1, 0.2+TAU*nm/2.0 + iTime*TAU/smallCount);
+  float d1 = cogwheel(cp1, 0.11, 0.125, smallCount, 5.0);
+
+  float l = length(p);
+  float d2 = l - (off+0.055);
+  float d3 = d2 + 0.020;;
+
+  vec2 tp0 = p;
+  modPolar(tp0, 60.0);
+  tp0.x -= off;
+  float d4 = box(tp0, vec2(0.0125, 0.005));
+
+  float ctime = -(iTime*0.05 + r)*TAU;
+
+  vec2 tp1 = p;
+  rot(tp1, ctime*12.0);
+  tp1.x -= 0.13;
+  float d5 = box(tp1, vec2(0.125, 0.005));
+
+  vec2 tp2 = p;
+  rot(tp2, ctime);
+  tp2.x -= 0.13*0.5;
+  float d6 = box(tp2, vec2(0.125*0.5, 0.0075));
+
+  float d7 = l - 0.025;
+  float d8 = l - 0.0125;
+
+  d = min(d, d0);
+  d = min(d, d1);
+  d = min(d, d2);
+  d = max(d, -d3);
+  d = min(d, d4);
+  d = min(d, d5);
+  d = min(d, d6);
+  d = min(d, d7);
+  d = max(d, -d8);
+
+  return d;
+}
+
+float df(vec2 p, float scale, inout vec2 nn) {
+  p /= scale;
+  nn = hextile(p);
+  nn = round(nn);
+  float r = hash(nn);
+
+  float d;;
+
+  if (r < 0.5) {
+    d = ccell1(p, r);
+  } else {
+    d = ccell2(p, r);
+  }
+
+  return d*scale;
+}
+
+vec3 postProcess(vec3 col, vec2 q)  {
+  //col = saturate(col);
+  col=pow(clamp(col,0.0,1.0),vec3(0.75));
+  col=col*0.6+0.4*col*col*(3.0-2.0*col);  // contrast
+  col=mix(col, vec3(dot(col, vec3(0.33))), -0.4);  // satuation
+  col*=0.5+0.5*pow(19.0*q.x*q.y*(1.0-q.x)*(1.0-q.y),0.7);  // vigneting
+  return col;
+}
+
+void mainImage(out vec4 fragColor, vec2 fragCoord) {
+  vec2 q = fragCoord/iResolution.xy;
+  vec2 p = -1.0 + 2.0*q;
+  p.x *= iResolution.x/iResolution.y;
+  float tm = iTime*0.1;
+  p += vec2(cos(tm), sin(tm*sqrt(0.5)));
+  float z = mix(0.5, 1.0, pcos(tm*sqrt(0.3)));
+  float aa = 4.0 / iResolution.y;
+
+  vec2 nn = vec2(0.0);
+  float d = df(p, z, nn);
+
+  vec3 col = vec3(160.0)/vec3(255.0);
+  vec3 baseCol = vec3(0.3);
+  vec4 logoCol = vec4(baseCol, 1.0)*smoothstep(-aa, 0.0, -d);
+  col = mix(col, logoCol.xyz, pow(logoCol.w, 8.0));
+  col += 0.4*pow(abs(sin(20.0*d)), 0.6);
+
+  col = postProcess(col, q);
+
+  fragColor = vec4(col, 1.0);
+}
diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml
index 7c9c657f5e..f179d9819b 100644
--- a/demos/gtk-demo/demo.gresource.xml
+++ b/demos/gtk-demo/demo.gresource.xml
@@ -121,6 +121,15 @@
     <file>gtkgears.c</file>
     <file>gtkgears.h</file>
   </gresource>
+  <gresource prefix="/shadertoy">
+    <file>gtkshadertoy.c</file>
+    <file>gtkshadertoy.h</file>
+    <file>alienplanet.glsl</file>
+    <file>mandelbrot.glsl</file>
+    <file>neon.glsl</file>
+    <file>cogs.glsl</file>
+    <file>glowingstars.glsl</file>
+  </gresource>
   <gresource prefix="/iconscroll">
     <file>iconscroll.ui</file>
   </gresource>
@@ -260,6 +269,7 @@
     <file>scale.c</file>
     <file>search_entry.c</file>
     <file>search_entry2.c</file>
+    <file>shadertoy.c</file>
     <file>shortcuts.c</file>
     <file>shortcut_triggers.c</file>
     <file>sizegroup.c</file>
diff --git a/demos/gtk-demo/glowingstars.glsl b/demos/gtk-demo/glowingstars.glsl
new file mode 100644
index 0000000000..3d73131024
--- /dev/null
+++ b/demos/gtk-demo/glowingstars.glsl
@@ -0,0 +1,174 @@
+// Originally from: https://www.shadertoy.com/view/ttBcRV
+// License CC0: Flying through glowing stars
+//  The result of playing around trying to improve an old shader
+
+#define PI              3.141592654
+#define TAU             (2.0*PI)
+#define TIME            iTime
+#define RESOLUTION      iResolution
+
+#define LESS(a,b,c)     mix(a,b,step(0.,c))
+#define SABS(x,k)       LESS((.5/(k))*(x)*(x)+(k)*.5,abs(x),abs(x)-(k))
+
+#define MROT(a) mat2(cos(a), sin(a), -sin(a), cos(a))
+
+vec3 hsv2rgb(vec3 c) {
+  const vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
+float hash(in vec3 co) {
+  return fract(sin(dot(co, vec3(12.9898,58.233, 12.9898+58.233))) * 13758.5453);
+}
+
+float starn(vec2 p, float r, int n, float m) {
+  // From IQ: https://www.shadertoy.com/view/3tSGDy
+  // https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
+
+  // Minor tweak to use SABS over abs to smooth inner corners
+  // SABS: https://www.shadertoy.com/view/Ws2SDK
+
+  // next 4 lines can be precomputed for a given shape
+  float an = 3.141593/float(n);
+  float en = 3.141593/m;  // m is between 2 and n
+  vec2  acs = vec2(cos(an),sin(an));
+  vec2  ecs = vec2(cos(en),sin(en)); // ecs=vec2(0,1) for regular polygon,
+
+  float bn = mod(atan(p.x,p.y),2.0*an) - an;
+  p = length(p)*vec2(cos(bn),SABS(sin(bn), 0.15));
+  p -= r*acs;
+  p += ecs*clamp( -dot(p,ecs), 0.0, r*acs.y/ecs.y);
+  return length(p)*sign(p.x);
+}
+
+vec4 alphaBlend(vec4 back, vec4 front) {
+  vec3 xyz = mix(back.xyz*back.w, front.xyz, front.w);
+  float w = mix(back.w, 1.0, front.w);
+  return vec4(xyz, w);
+}
+
+void rot(inout vec2 p, float a) {
+  float c = cos(a);
+  float s = sin(a);
+  p = vec2(c*p.x + s*p.y, -s*p.x + c*p.y);
+}
+
+vec3 offset(float z) {
+  float a = z;
+  vec2 p = -0.075*(vec2(cos(a), sin(a*sqrt(2.0))) + vec2(cos(a*sqrt(0.75)), sin(a*sqrt(0.5))));
+  return vec3(p, z);
+}
+
+vec3 doffset(float z) {
+  float eps = 0.05;
+  return 0.5*(offset(z + eps) - offset(z - eps))/eps;
+}
+
+vec3 ddoffset(float z) {
+  float eps = 0.05;
+  return 0.5*(doffset(z + eps) - doffset(z - eps))/eps;
+}
+
+vec4 planeCol(vec3 ro, vec3 rd, float n, vec3 pp) {
+  const float s = 0.5;
+
+  vec2 p = pp.xy;
+  float z = pp.z;
+  vec2 dpy = dFdy(p);
+  float aa = length(dpy);
+
+  p -= (1.0+5.0*(pp.z - ro.z))*offset(z).xy;
+
+  p *= s;
+  float r = hash(vec3(floor(p+0.5), n));
+  p = fract(p+0.5)-0.5;
+  rot(p, ((TAU*r+n)*0.25));
+  float d = starn(p, 0.20, 3 + 2*int(3.0*r), 3.0);
+  d -= 0.06;
+  d/=s;
+
+  float ds = -d+0.03;
+  vec3 cols = hsv2rgb(vec3(337.0/360.0+0.1*sin(n*0.3), 0.8, 0.54+0.2*sin(n*0.3)));
+  float ts = 1.0 - smoothstep(-aa, 0.0, ds);
+  vec4 cs =  vec4(cols, ts*0.93);
+
+  float db = abs(d) - (0.06);
+  db = abs(db) - 0.03;
+  db = abs(db) - 0.00;
+  db = max(db, -d+0.03);
+    vec3 colb = vec3(1.0, 0.7, 0.5);
+  float tb = exp(-(db)*30.0*(1.0 - 10.0*aa));
+  vec4 cb = vec4(1.5*colb, tb);
+
+  vec4 ct = alphaBlend(cs, cb);
+
+  return ct;
+}
+
+vec3 color(vec3 ww, vec3 uu, vec3 vv, vec3 ro, vec2 p) {
+  vec3 rd = normalize(p.x*uu + p.y*vv + (2.0-tanh(length(p)))*ww);
+
+  vec4 col = vec4(vec3(0.0), 1.0);
+
+  const float planeDist = 1.0;
+  const int furthest = 6;
+  const int fadeFrom = furthest-3;
+
+  float nz = floor(ro.z / planeDist);
+
+  for (int i = furthest; i >= 1; --i) {
+    float pz = planeDist*nz + planeDist*float(i);
+
+    float pd = (pz - ro.z)/rd.z;
+
+    if (pd > 0.0) {
+      vec3 pp = ro + rd*pd;
+
+      vec4 pcol = planeCol(ro, rd, nz+float(i), pp);
+      float fadeIn = 1.0-smoothstep(planeDist*float(fadeFrom), planeDist*float(furthest), pp.z-ro.z);
+      pcol.xyz *= sqrt(fadeIn);
+
+      col = alphaBlend(col, pcol);
+    }
+  }
+
+  return col.xyz*col.w;
+}
+
+vec3 postProcess(vec3 col, vec2 q)  {
+  col=pow(clamp(col,0.0,1.0),vec3(0.75));
+  col=col*0.6+0.4*col*col*(3.0-2.0*col);
+  col=mix(col, vec3(dot(col, vec3(0.33))), -0.4);
+  col*=0.5+0.5*pow(19.0*q.x*q.y*(1.0-q.x)*(1.0-q.y),0.7);
+  return col;
+}
+
+vec3 effect(vec2 p, vec2 q) {
+  float tm = TIME*0.65;
+
+  vec3 ro   = offset(tm);
+  vec3 dro  = doffset(tm);
+  vec3 ddro = ddoffset(tm);
+
+  vec3 ww = normalize(dro);
+  vec3 uu = normalize(cross(vec3(0.0,1.0,0.0)+1.5*ddro, ww));
+  vec3 vv = normalize(cross(ww, uu));
+
+  vec3 col = color(ww, uu, vv, ro, p);
+  col = postProcess(col, q);
+
+  const float fadeIn = 2.0;
+
+  return col*smoothstep(0.0, fadeIn, TIME);
+}
+
+void mainImage(out vec4 fragColor, vec2 fragCoord) {
+  vec2 q = fragCoord/RESOLUTION.xy;
+  vec2 p = -1. + 2. * q;
+  p.x *= RESOLUTION.x/RESOLUTION.y;
+
+  vec3 col = effect(p, q);
+
+  fragColor = vec4(col, 1.0);
+}
diff --git a/demos/gtk-demo/gtkshadertoy.c b/demos/gtk-demo/gtkshadertoy.c
new file mode 100644
index 0000000000..866bd69481
--- /dev/null
+++ b/demos/gtk-demo/gtkshadertoy.c
@@ -0,0 +1,524 @@
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+#include <epoxy/gl.h>
+
+#include "gtkshadertoy.h"
+
+const char *default_image_shader =
+  "void mainImage(out vec4 fragColor, in vec2 fragCoord) {\n"
+  "    // Normalized pixel coordinates (from 0 to 1)\n"
+  "    vec2 uv = fragCoord/iResolution.xy;\n"
+  "\n"
+  "   // Time varying pixel color\n"
+  "    vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));\n"
+  "\n"
+  "    if (distance(iMouse.xy, fragCoord.xy) <= 10.0) {\n"
+  "        col = vec3(0.0);\n"
+  "    }\n"
+  "\n"
+  "    // Output to screen\n"
+  "    fragColor = vec4(col,1.0);\n"
+  "}\n";
+
+const char *shadertoy_vertex_shader =
+  "#version 150 core\n"
+  "\n"
+  "uniform vec3 iResolution;\n"
+  "\n"
+  "in vec2 position;\n"
+  "out vec2 fragCoord;\n"
+  "\n"
+  "void main() {\n"
+  "    gl_Position = vec4(position, 0.0, 1.0);\n"
+  "\n"
+  "    // Convert from OpenGL coordinate system (with origin in center\n"
+  "    // of screen) to Shadertoy/texture coordinate system (with origin\n"
+  "    // in lower left corner)\n"
+  "    fragCoord = (gl_Position.xy + vec2(1.0)) / vec2(2.0) * iResolution.xy;\n"
+  "}\n";
+
+const char *fragment_prefix =
+  "#version 150 core\n"
+  "\n"
+  "uniform vec3      iResolution;           // viewport resolution (in pixels)\n"
+  "uniform float     iTime;                 // shader playback time (in seconds)\n"
+  "uniform float     iTimeDelta;            // render time (in seconds)\n"
+  "uniform int       iFrame;                // shader playback frame\n"
+  "uniform float     iChannelTime[4];       // channel playback time (in seconds)\n"
+  "uniform vec3      iChannelResolution[4]; // channel resolution (in pixels)\n"
+  "uniform vec4      iMouse;                // mouse pixel coords. xy: current (if MLB down), zw: click\n"
+  "uniform sampler2D iChannel0;\n"
+  "uniform sampler2D iChannel1;\n"
+  "uniform sampler2D iChannel2;\n"
+  "uniform sampler2D iChannel3;\n"
+  "uniform vec4      iDate;                 // (year, month, day, time in seconds)\n"
+  "uniform float     iSampleRate;           // sound sample rate (i.e., 44100)\n"
+  "\n"
+  "in vec2 fragCoord;\n"
+  "out vec4 fragColor;\n";
+
+
+// Fragment shader suffix
+const char *fragment_suffix =
+  "    void main() {\n"
+  "        mainImage(fragColor, fragCoord);\n"
+  "    }\n";
+
+typedef struct {
+  char *image_shader;
+  gboolean image_shader_dirty;
+
+  gboolean error_set;
+
+  /* Vertex buffers */
+  GLuint vao;
+  GLuint buffer;
+
+  /* Active program */
+  GLuint program;
+
+  /* Location of uniforms for program */
+  GLuint resolution_location;
+  GLuint time_location;
+  GLuint timedelta_location;
+  GLuint frame_location;
+  GLuint mouse_location;
+
+  /* Current uniform values */
+  float resolution[3];
+  float time;
+  float timedelta;
+  float mouse[4];
+  int frame;
+
+  /* Animation data */
+  gint64 first_frame_time;
+  gint64 first_frame;
+  guint tick;
+} GtkShadertoyPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtkShadertoy, gtk_shadertoy, GTK_TYPE_GL_AREA)
+
+static gboolean gtk_shadertoy_render        (GtkGLArea     *area,
+                                             GdkGLContext  *context);
+static void     gtk_shadertoy_reshape       (GtkGLArea     *area,
+                                             int            width,
+                                             int            height);
+static void     gtk_shadertoy_realize       (GtkWidget     *widget);
+static void     gtk_shadertoy_unrealize     (GtkWidget     *widget);
+static gboolean gtk_shadertoy_tick          (GtkWidget     *widget,
+                                             GdkFrameClock *frame_clock,
+                                             gpointer       user_data);
+
+GtkWidget *
+gtk_shadertoy_new (void)
+{
+  return g_object_new (gtk_shadertoy_get_type (), NULL);
+}
+
+static void
+drag_begin_cb (GtkGestureDrag *drag,
+               double          x,
+               double          y,
+               gpointer        user_data)
+{
+  GtkShadertoy *shadertoy = GTK_SHADERTOY (user_data);
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+  int height = gtk_widget_get_height (GTK_WIDGET (shadertoy));
+  int scale = gtk_widget_get_scale_factor (GTK_WIDGET (shadertoy));
+
+ priv->mouse[0] = x * scale;
+ priv->mouse[1] = (height - y) * scale;
+ priv->mouse[2] = priv->mouse[0];
+ priv->mouse[3] = priv->mouse[1];
+}
+
+static void
+drag_update_cb (GtkGestureDrag *drag,
+                double          dx,
+                double          dy,
+                gpointer        user_data)
+{
+  GtkShadertoy *shadertoy = GTK_SHADERTOY (user_data);
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+  int width = gtk_widget_get_width (GTK_WIDGET (shadertoy));
+  int height = gtk_widget_get_height (GTK_WIDGET (shadertoy));
+  int scale = gtk_widget_get_scale_factor (GTK_WIDGET (shadertoy));
+  double x, y;
+
+  gtk_gesture_drag_get_start_point (drag, &x, &y);
+  x += dx;
+  y += dy;
+
+  if (x >= 0 && x < width &&
+      y >= 0 && y < height)
+    {
+      priv->mouse[0] = x * scale;
+      priv->mouse[1] = (height - y) * scale;
+    }
+}
+
+static void
+drag_end_cb (GtkGestureDrag *drag,
+             gdouble         dx,
+             gdouble         dy,
+             gpointer        user_data)
+{
+  GtkShadertoy *shadertoy = GTK_SHADERTOY (user_data);
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+
+  priv->mouse[2] = -priv->mouse[2];
+  priv->mouse[3] = -priv->mouse[3];
+}
+
+static void
+gtk_shadertoy_init (GtkShadertoy *shadertoy)
+{
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+  GtkGesture *drag;
+
+  priv->image_shader = g_strdup (default_image_shader);
+  priv->tick = gtk_widget_add_tick_callback (GTK_WIDGET (shadertoy), gtk_shadertoy_tick, shadertoy, NULL);
+
+  drag = gtk_gesture_drag_new ();
+  gtk_widget_add_controller (GTK_WIDGET (shadertoy), GTK_EVENT_CONTROLLER (drag));
+  g_signal_connect (drag, "drag-begin", (GCallback)drag_begin_cb, shadertoy);
+  g_signal_connect (drag, "drag-update", (GCallback)drag_update_cb, shadertoy);
+  g_signal_connect (drag, "drag-end", (GCallback)drag_end_cb, shadertoy);
+}
+
+static void
+gtk_shadertoy_finalize (GObject *obj)
+{
+  GtkShadertoy *shadertoy = GTK_SHADERTOY (obj);
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+
+  gtk_widget_remove_tick_callback (GTK_WIDGET (shadertoy), priv->tick);
+  g_free (priv->image_shader);
+
+  G_OBJECT_CLASS (gtk_shadertoy_parent_class)->finalize (obj);
+}
+
+static void
+gtk_shadertoy_class_init (GtkShadertoyClass *klass)
+{
+  GTK_GL_AREA_CLASS (klass)->render = gtk_shadertoy_render;
+  GTK_GL_AREA_CLASS (klass)->resize = gtk_shadertoy_reshape;
+
+  GTK_WIDGET_CLASS (klass)->realize = gtk_shadertoy_realize;
+  GTK_WIDGET_CLASS (klass)->unrealize = gtk_shadertoy_unrealize;
+
+  G_OBJECT_CLASS (klass)->finalize = gtk_shadertoy_finalize;
+}
+
+/* new window size or exposure */
+static void
+gtk_shadertoy_reshape (GtkGLArea *area, int width, int height)
+{
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private ((GtkShadertoy *) area);
+
+  priv->resolution[0] = width;
+  priv->resolution[1] = height;
+  priv->resolution[2] = 1.0; /* screen aspect ratio */
+
+  /* Set the viewport */
+  glViewport (0, 0, (GLint) width, (GLint) height);
+}
+
+static GLuint
+create_shader (int         type,
+               const char *src,
+               GError    **error)
+{
+  GLuint shader;
+  int status;
+
+  shader = glCreateShader (type);
+  glShaderSource (shader, 1, &src, NULL);
+  glCompileShader (shader);
+
+  glGetShaderiv (shader, GL_COMPILE_STATUS, &status);
+  if (status == GL_FALSE)
+    {
+      int log_len;
+      char *buffer;
+
+      glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &log_len);
+
+      buffer = g_malloc (log_len + 1);
+      glGetShaderInfoLog (shader, log_len, NULL, buffer);
+
+      g_set_error (error, GDK_GL_ERROR, GDK_GL_ERROR_COMPILATION_FAILED,
+                   "Compile failure in %s shader:\n%s",
+                   type == GL_VERTEX_SHADER ? "vertex" : "fragment",
+                   buffer);
+
+      g_free (buffer);
+
+      glDeleteShader (shader);
+
+      return 0;
+    }
+
+  return shader;
+}
+
+static gboolean
+init_shaders (GtkShadertoy *shadertoy,
+              const char *vertex_source,
+              const char *fragment_source,
+              GError **error)
+{
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+  GLuint vertex, fragment;
+  GLuint program = 0;
+  int status;
+  gboolean res = TRUE;
+
+  vertex = create_shader (GL_VERTEX_SHADER, vertex_source, error);
+  if (vertex == 0)
+    return FALSE;
+
+  fragment = create_shader (GL_FRAGMENT_SHADER, fragment_source, error);
+  if (fragment == 0)
+    {
+      glDeleteShader (vertex);
+      return FALSE;
+    }
+
+  program = glCreateProgram ();
+  glAttachShader (program, vertex);
+  glAttachShader (program, fragment);
+
+  glLinkProgram (program);
+
+  glGetProgramiv (program, GL_LINK_STATUS, &status);
+  if (status == GL_FALSE)
+    {
+      int log_len;
+      char *buffer;
+
+      glGetProgramiv (program, GL_INFO_LOG_LENGTH, &log_len);
+
+      buffer = g_malloc (log_len + 1);
+      glGetProgramInfoLog (program, log_len, NULL, buffer);
+
+      g_set_error (error, GDK_GL_ERROR, GDK_GL_ERROR_LINK_FAILED,
+                   "Linking failure:\n%s", buffer);
+      res = FALSE;
+
+      g_free (buffer);
+
+      glDeleteProgram (program);
+
+      goto out;
+    }
+
+  if (priv->program != 0)
+    glDeleteProgram (priv->program);
+
+  priv->program = program;
+  priv->resolution_location = glGetUniformLocation (program, "iResolution");
+  priv->time_location = glGetUniformLocation (program, "iTime");
+  priv->timedelta_location = glGetUniformLocation (program, "iTimeDelta");
+  priv->frame_location = glGetUniformLocation (program, "iFrame");
+  priv->mouse_location = glGetUniformLocation (program, "iMouse");
+
+  glDetachShader (program, vertex);
+  glDetachShader (program, fragment);
+
+out:
+  /* These are now owned by the program and can be deleted */
+  glDeleteShader (vertex);
+  glDeleteShader (fragment);
+
+  return res;
+}
+
+static void
+gtk_shadertoy_realize_shader (GtkShadertoy *shadertoy)
+{
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+  char *fragment_shader;
+  GError *error = NULL;
+
+  fragment_shader = g_strconcat (fragment_prefix, priv->image_shader, fragment_suffix, NULL);
+  if (!init_shaders (shadertoy, shadertoy_vertex_shader, fragment_shader, &error))
+    {
+      priv->error_set = TRUE;
+      gtk_gl_area_set_error (GTK_GL_AREA (shadertoy), error);
+      g_error_free (error);
+    }
+  g_free (fragment_shader);
+
+  /* Start new shader at time zero */
+  priv->first_frame_time = 0;
+  priv->first_frame = 0;
+
+  priv->image_shader_dirty = FALSE;
+}
+
+static gboolean
+gtk_shadertoy_render (GtkGLArea    *area,
+                      GdkGLContext *context)
+{
+  GtkShadertoy *shadertoy = GTK_SHADERTOY (area);
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+
+  if (gtk_gl_area_get_error (area) != NULL)
+    return FALSE;
+
+  if (priv->image_shader_dirty)
+    gtk_shadertoy_realize_shader (shadertoy);
+
+  /* Clear the viewport */
+  glClearColor (0.0, 0.0, 0.0, 1.0);
+  glClear (GL_COLOR_BUFFER_BIT);
+
+  glUseProgram (priv->program);
+
+  /* Update uniforms */
+  if (priv->resolution_location != -1)
+    glUniform3fv (priv->resolution_location, 1, priv->resolution);
+  if (priv->time_location != -1)
+    glUniform1f (priv->time_location, priv->time);
+  if (priv->timedelta_location != -1)
+    glUniform1f (priv->timedelta_location, priv->timedelta);
+  if (priv->frame_location != -1)
+    glUniform1i (priv->frame_location, priv->frame);
+  if (priv->mouse_location != -1)
+    glUniform4fv (priv->mouse_location, 1, priv->mouse);
+
+  /* Use the vertices in our buffer */
+  glBindBuffer (GL_ARRAY_BUFFER, priv->buffer);
+  glEnableVertexAttribArray (0);
+  glVertexAttribPointer (0, 4, GL_FLOAT, GL_FALSE, 0, 0);
+
+  glDrawArrays (GL_TRIANGLES, 0, 6);
+
+  /* We finished using the buffers and program */
+  glDisableVertexAttribArray (0);
+  glBindBuffer (GL_ARRAY_BUFFER, 0);
+  glUseProgram (0);
+
+  /* Flush the contents of the pipeline */
+  glFlush ();
+
+  return TRUE;
+}
+
+const char *
+gtk_shadertoy_get_image_shader (GtkShadertoy *shadertoy)
+{
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+
+  return priv->image_shader;
+}
+
+void
+gtk_shadertoy_set_image_shader (GtkShadertoy *shadertoy,
+                                const char   *shader)
+{
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+
+  g_free (priv->image_shader);
+  priv->image_shader = g_strdup (shader);
+
+  /* Don't override error we didn't set it ourselves */
+  if (priv->error_set)
+    {
+      gtk_gl_area_set_error (GTK_GL_AREA (shadertoy), NULL);
+      priv->error_set = FALSE;
+    }
+  priv->image_shader_dirty = TRUE;
+}
+
+static void
+gtk_shadertoy_realize (GtkWidget *widget)
+{
+  GtkGLArea *glarea = GTK_GL_AREA (widget);
+  GtkShadertoy *shadertoy = GTK_SHADERTOY (widget);
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+
+  /* Draw two triangles across whole screen */
+  const GLfloat vertex_data[] = {
+    -1.0f, -1.0f, 0.f, 1.f,
+    -1.0f,  1.0f, 0.f, 1.f,
+    1.0f,  1.0f, 0.f, 1.f,
+
+    -1.0f, -1.0f, 0.f, 1.f,
+    1.0f,  1.0f, 0.f, 1.f,
+    1.0f, -1.0f, 0.f, 1.f,
+  };
+
+  GTK_WIDGET_CLASS (gtk_shadertoy_parent_class)->realize (widget);
+
+  gtk_gl_area_make_current (glarea);
+  if (gtk_gl_area_get_error (glarea) != NULL)
+    return;
+
+  glGenVertexArrays (1, &priv->vao);
+  glBindVertexArray (priv->vao);
+
+  glGenBuffers (1, &priv->buffer);
+  glBindBuffer (GL_ARRAY_BUFFER, priv->buffer);
+  glBufferData (GL_ARRAY_BUFFER, sizeof (vertex_data), vertex_data, GL_STATIC_DRAW);
+  glBindBuffer (GL_ARRAY_BUFFER, 0);
+
+  gtk_shadertoy_realize_shader (shadertoy);
+}
+
+static void
+gtk_shadertoy_unrealize (GtkWidget *widget)
+{
+  GtkGLArea *glarea = GTK_GL_AREA (widget);
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private ((GtkShadertoy *) widget);
+
+  gtk_gl_area_make_current (glarea);
+  if (gtk_gl_area_get_error (glarea) == NULL)
+    {
+      if (priv->buffer != 0)
+        glDeleteBuffers (1, &priv->buffer);
+
+      if (priv->vao != 0)
+        glDeleteVertexArrays (1, &priv->vao);
+
+      if (priv->program != 0)
+        glDeleteProgram (priv->program);
+    }
+
+  GTK_WIDGET_CLASS (gtk_shadertoy_parent_class)->unrealize (widget);
+}
+
+static gboolean
+gtk_shadertoy_tick (GtkWidget     *widget,
+                    GdkFrameClock *frame_clock,
+                    gpointer       user_data)
+{
+  GtkShadertoy *shadertoy = GTK_SHADERTOY (widget);
+  GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
+  gint64 frame_time;
+  gint64 frame;
+  float previous_time;
+
+  frame = gdk_frame_clock_get_frame_counter (frame_clock);
+  frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+  if (priv->first_frame_time == 0)
+    {
+      priv->first_frame_time = frame_time;
+      priv->first_frame = frame;
+      previous_time = 0;
+    }
+  else
+    previous_time = priv->time;
+
+  priv->time = (frame_time - priv->first_frame_time) / 1000000.0f;
+  priv->frame = frame - priv->first_frame;
+  priv->timedelta = priv->time - previous_time;
+
+  gtk_widget_queue_draw (widget);
+
+  return G_SOURCE_CONTINUE;
+}
diff --git a/demos/gtk-demo/gtkshadertoy.h b/demos/gtk-demo/gtkshadertoy.h
new file mode 100644
index 0000000000..38f48995b8
--- /dev/null
+++ b/demos/gtk-demo/gtkshadertoy.h
@@ -0,0 +1,34 @@
+#ifndef __GTK_SHADERTOY_H__
+#define __GTK_SHADERTOY_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_SHADERTOY      (gtk_shadertoy_get_type ())
+#define GTK_SHADERTOY(inst)     (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+                                 GTK_TYPE_SHADERTOY,                 \
+                                 GtkShadertoy))
+#define GTK_IS_SHADERTOY(inst)  (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+                                 GTK_TYPE_SHADERTOY))
+
+typedef struct _GtkShadertoy GtkShadertoy;
+typedef struct _GtkShadertoyClass GtkShadertoyClass;
+
+struct _GtkShadertoy {
+  GtkGLArea parent;
+};
+
+struct _GtkShadertoyClass {
+  GtkGLAreaClass parent_class;
+};
+
+GType       gtk_shadertoy_get_type         (void) G_GNUC_CONST;
+GtkWidget  *gtk_shadertoy_new              (void);
+const char *gtk_shadertoy_get_image_shader (GtkShadertoy *shadertoy);
+void        gtk_shadertoy_set_image_shader (GtkShadertoy *shadertoy,
+                                            const char   *shader);
+
+G_END_DECLS
+
+#endif /* __GTK_SHADERTOY_H__ */
diff --git a/demos/gtk-demo/mandelbrot.glsl b/demos/gtk-demo/mandelbrot.glsl
new file mode 100644
index 0000000000..0a0c1ee3fd
--- /dev/null
+++ b/demos/gtk-demo/mandelbrot.glsl
@@ -0,0 +1,95 @@
+// Originally from: https://www.shadertoy.com/view/wdBfDK
+// License: CC0
+
+#define MANDELBROT_ZOOM_START 0.0
+#define MANDELBROT_ITER       240
+
+void pR(inout vec2 p, in float a) {
+  p = cos(a)*p + sin(a)*vec2(p.y, -p.x);
+}
+
+vec2 pMod2(inout vec2 p, in vec2 size) {
+  vec2 c = floor((p + size*0.5)/size);
+  p = mod(p + size*0.5,size) - size*0.5;
+  return c;
+}
+
+
+vec3 mandelbrot(float time, vec2 p, out float ii) {
+  vec3 col = vec3(0.0);
+
+  float ztime = (time - MANDELBROT_ZOOM_START)*step(MANDELBROT_ZOOM_START, time);
+
+  float zoo = 0.64 + 0.36*cos(.07*ztime);
+  float coa = cos(0.15*(1.0-zoo)*ztime);
+  float sia = sin(0.15*(1.0-zoo)*ztime);
+  zoo = pow(zoo,8.0);
+  vec2 xy = vec2( p.x*coa-p.y*sia, p.x*sia+p.y*coa);
+  vec2 c = vec2(-.745,.186) + xy*zoo;
+
+  const float B = 10.0;
+  float l = 0.0;
+  vec2 z  = vec2(0.0);
+
+  vec2 zc = vec2(1.0);
+
+  pR(zc, ztime);
+
+  float d = 1e20;
+
+  int i = 0;
+
+  for(int j = 0; j < MANDELBROT_ITER; ++j) {
+    float re2 = z.x*z.x;
+    float im2 = z.y*z.y;
+    float reim= z.x*z.y;
+
+    if(re2 + im2 > (B*B)) break;
+
+    z = vec2(re2 - im2, 2.0*reim) + c;
+
+    vec2 zm = z;
+    vec2 n = pMod2(zm, vec2(4));
+    vec2 pp = zm - zc;
+    float dd = dot(pp, pp);
+
+    d = min(d, dd);
+
+    l += 1.0;
+
+    i = j;
+  }
+
+  ii = float(i)/float(MANDELBROT_ITER);
+
+  float sl = l - log2(log2(dot(z,z))) + 4.0;
+
+  vec3 dc = vec3(pow(max(1.0 - d, 0.0), 20.0));
+  vec3 gc = 0.5 + 0.5*cos(3.0 + sl*0.15 + vec3(0.1,0.5,0.9));
+  return gc + dc*smoothstep(28.8, 29.0, ztime);
+}
+
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+  float s = 2.0/iResolution.y;
+
+  vec2 o1 = vec2(1.0/8.0, 3.0/8.0)*s;
+  vec2 o2 = vec2(-3.0/8.0, 1.0/8.0)*s;
+
+  vec2 p = (-iResolution.xy + 2.0*fragCoord.xy)/iResolution.y;
+  float ii = 0.0;
+  vec3 col = mandelbrot(iTime, p+o1, ii);
+
+  // "smart" AA? Is that a good idea?
+  vec2 dii2 = vec2(dFdx(ii), dFdy(ii));
+  float dii = length(dii2);
+
+  if(abs(dii) > 0.01) {
+    col += mandelbrot(iTime, p-o1, ii);
+    col += mandelbrot(iTime, p+o2, ii);
+    col += mandelbrot(iTime, p-o2, ii);
+    col *=0.25;
+//    col = vec3(1.0, 0.0, 0.0);
+  }
+
+  fragColor = vec4(col, 1.0);
+}
diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build
index 05b32c6f46..9ee9777d13 100644
--- a/demos/gtk-demo/meson.build
+++ b/demos/gtk-demo/meson.build
@@ -70,6 +70,7 @@ demos = files([
   'scale.c',
   'search_entry.c',
   'search_entry2.c',
+  'shadertoy.c',
   'shortcuts.c',
   'shortcut_triggers.c',
   'sidebar.c',
@@ -98,6 +99,7 @@ extra_demo_sources = files(['main.c',
                             'gtkfishbowl.c',
                             'fontplane.c',
                             'gtkgears.c',
+                            'gtkshadertoy.c',
                             'puzzlepiece.c',
                             'bluroverlay.c',
                             'demoimage.c',
diff --git a/demos/gtk-demo/neon.glsl b/demos/gtk-demo/neon.glsl
new file mode 100644
index 0000000000..b42790efe9
--- /dev/null
+++ b/demos/gtk-demo/neon.glsl
@@ -0,0 +1,220 @@
+// Originally from: https://www.shadertoy.com/view/WlByzy
+// License CC0: Neonwave style road, sun and city
+//  The result of a bit of experimenting with neonwave style colors.
+
+#define PI          3.141592654
+#define TAU         (2.0*PI)
+
+#define TIME        iTime
+#define RESOLUTION  iResolution
+
+vec3 hsv2rgb(vec3 c) {
+  const vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+}
+
+float hash(in float co) {
+  return fract(sin(co*12.9898) * 13758.5453);
+}
+
+float hash(in vec2 co) {
+  return fract(sin(dot(co.xy ,vec2(12.9898,58.233))) * 13758.5453);
+}
+
+float psin(float a) {
+  return 0.5 + 0.5*sin(a);
+}
+
+float mod1(inout float p, float size) {
+  float halfsize = size*0.5;
+  float c = floor((p + halfsize)/size);
+  p = mod(p + halfsize, size) - halfsize;
+  return c;
+}
+
+float circle(vec2 p, float r) {
+  return length(p) - r;
+}
+
+float box(vec2 p, vec2 b) {
+  vec2 d = abs(p)-b;
+  return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
+}
+
+float planex(vec2 p, float w) {
+  return abs(p.y) - w;
+}
+
+float planey(vec2 p, float w) {
+  return abs(p.x) - w;
+}
+
+float pmin(float a, float b, float k) {
+  float h = clamp( 0.5+0.5*(b-a)/k, 0.0, 1.0 );
+  return mix( b, a, h ) - k*h*(1.0-h);
+}
+
+float pmax(float a, float b, float k) {
+  return -pmin(-a, -b, k);
+}
+
+float sun(vec2 p) {
+  const float ch = 0.0125;
+  vec2 sp = p;
+  vec2 cp = p;
+  mod1(cp.y, ch*6.0);
+
+  float d0 = circle(sp, 0.5);
+  float d1 = planex(cp, ch);
+  float d2 = p.y+ch*3.0;
+
+  float d = d0;
+  d = pmax(d, -max(d1, d2), ch*2.0);
+
+  return d;
+}
+
+float city(vec2 p) {
+  float sd = circle(p, 0.5);
+  float cd = 1E6;
+
+  const float count = 5.0;
+  const float width = 0.1;
+
+  for (float i = 0.0; i < count; ++i) {
+    vec2 pp = p;
+    pp.x += i*width/count;
+    float nn = mod1(pp.x, width);
+    float rr = hash(nn+sqrt(3.0)*i);
+    float dd = box(pp-vec2(0.0, -0.5), vec2(0.02, 0.35*(1.0-smoothstep(0.0, 5.0, abs(nn)))*rr+0.1));
+    cd = min(cd, dd);
+  }
+
+  return max(sd,cd);
+}
+vec3 sunEffect(vec2 p) {
+  float aa = 4.0 / RESOLUTION.y;
+
+  vec3 col = vec3(0.1);
+  vec3 skyCol1 = hsv2rgb(vec3(283.0/360.0, 0.83, 0.16));
+  vec3 skyCol2 = hsv2rgb(vec3(297.0/360.0, 0.79, 0.43));
+  col = mix(skyCol1, skyCol2, pow(clamp(0.5*(1.0+p.y+0.1*sin(4.0*p.x+TIME*0.5)), 0.0, 1.0), 4.0));
+
+  p.y -= 0.375;
+  float ds = sun(p);
+  float dc = city(p);
+
+  float dd = circle(p, 0.5);
+
+  vec3 sunCol = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 1.0), clamp(0.5 - 1.0*p.y, 0.0, 1.0));
+  vec3 glareCol = sqrt(sunCol);
+  vec3 cityCol = sunCol*sunCol;
+
+  col += glareCol*(exp(-30.0*ds))*step(0.0, ds);
+
+
+  float t1 = smoothstep(0.0, 0.075, -dd);
+  float t2 = smoothstep(0.0, 0.3, -dd);
+  col = mix(col, sunCol, smoothstep(-aa, 0.0, -ds));
+  col = mix(col, glareCol, smoothstep(-aa, 0.0, -dc)*t1);
+  col += vec3(0.0, 0.25, 0.0)*(exp(-90.0*dc))*step(0.0, dc)*t2;
+
+//  col += 0.3*psin(d*400);
+
+  return col;
+}
+
+float ground(vec2 p) {
+  p.y += TIME*80.0;
+  p *= 0.075;
+  vec2 gp = p;
+  gp = fract(gp) - vec2(0.5);
+  float d0 = abs(gp.x);
+  float d1 = abs(gp.y);
+  float d2 = circle(gp, 0.05);
+
+  const float rw = 2.5;
+  const float sw = 0.0125;
+
+  vec2 rp = p;
+  mod1(rp.y, 12.0);
+  float d3 = abs(rp.x) - rw;
+  float d4 = abs(d3) - sw*2.0;
+  float d5 = box(rp, vec2(sw*2.0, 2.0));
+  vec2 sp = p;
+  mod1(sp.y, 4.0);
+  sp.x = abs(sp.x);
+  sp -= vec2(rw - 0.125, 0.0);
+  float d6 = box(sp, vec2(sw, 1.0));
+
+  float d = d0;
+  d = pmin(d, d1, 0.1);
+  d = max(d, -d3);
+  d = min(d, d4);
+  d = min(d, d5);
+  d = min(d, d6);
+
+  return d;
+}
+
+vec3 groundEffect(vec2 p) {
+  vec3 ro = vec3(0.0, 20.0, 0.0);
+  vec3 ww = normalize(vec3(0.0, -0.025, 1.0));
+  vec3 uu = normalize(cross(vec3(0.0,1.0,0.0), ww));
+  vec3 vv = normalize(cross(ww,uu));
+  vec3 rd = normalize(p.x*uu + p.y*vv + 2.5*ww);
+
+  float distg = (-9.0 - ro.y)/rd.y;
+
+  const vec3 shineCol = 0.75*vec3(0.5, 0.75, 1.0);
+  const vec3 gridCol = vec3(1.0);
+
+  vec3 col = vec3(0.0);
+  if (distg > 0.0) {
+    vec3 pg = ro + rd*distg;
+    float aa = length(dFdx(pg))*0.0002*RESOLUTION.x;
+
+    float dg = ground(pg.xz);
+
+    col = mix(col, gridCol, smoothstep(-aa, 0.0, -(dg+0.0175)));
+    col += shineCol*(exp(-10.0*clamp(dg, 0.0, 1.0)));
+    col = clamp(col, 0.0, 1.0);
+
+//    col += 0.3*psin(dg*100);
+    col *= pow(1.0-smoothstep(ro.y*3.0, 220.0+ro.y*2.0, distg), 2.0);
+  }
+
+  return col;
+}
+
+vec3 postProcess(vec3 col, vec2 q)  {
+  col = clamp(col,0.0,1.0);
+//  col=pow(col,vec3(0.75));
+  col=col*0.6+0.4*col*col*(3.0-2.0*col);
+  col=mix(col, vec3(dot(col, vec3(0.33))), -0.4);
+  col*=0.5+0.5*pow(19.0*q.x*q.y*(1.0-q.x)*(1.0-q.y),0.7);
+  return col;
+}
+
+vec3 effect(vec2 p, vec2 q) {
+  vec3 col = vec3(0.0);
+
+  vec2 off = vec2(0.0, 0.0);
+
+  col += sunEffect(p+off);
+  col += groundEffect(p+off);
+
+  col = postProcess(col, q);
+  return col;
+}
+
+void mainImage(out vec4 fragColor, vec2 fragCoord) {
+  vec2 q = fragCoord/iResolution.xy;
+  vec2 p = -1. + 2. * q;
+  p.x *= RESOLUTION.x / RESOLUTION.y;
+
+  vec3 col = effect(p, q);
+
+  fragColor = vec4(col, 1.0);
+}
diff --git a/demos/gtk-demo/shadertoy.c b/demos/gtk-demo/shadertoy.c
new file mode 100644
index 0000000000..f6cad85ad3
--- /dev/null
+++ b/demos/gtk-demo/shadertoy.c
@@ -0,0 +1,191 @@
+/* OpenGL/Shadertoy
+ *
+ * Generate pixels using a custom fragment shader.
+ *
+ * The names of the uniforms are compatible with the shaders on shadertoy.com, so
+ * many of the shaders there work here too.
+ */
+#include <math.h>
+#include <gtk/gtk.h>
+#include <epoxy/gl.h>
+#include "gtkshadertoy.h"
+
+static GtkWidget *demo_window = NULL;
+static GtkWidget *shadertoy = NULL;
+static GtkTextBuffer *textbuffer = NULL;
+
+static void
+run (void)
+{
+  GtkTextIter start, end;
+  char *text;
+
+  gtk_text_buffer_get_bounds (textbuffer, &start, &end);
+  text = gtk_text_buffer_get_text (textbuffer, &start, &end, FALSE);
+
+  gtk_shadertoy_set_image_shader (GTK_SHADERTOY (shadertoy), text);
+  g_free (text);
+}
+
+static void
+run_clicked_cb (GtkWidget *button,
+                    gpointer user_data)
+{
+  run ();
+}
+
+static void
+load_clicked_cb (GtkWidget *button,
+                 gpointer user_data)
+{
+  const char *path = user_data;
+  GBytes *initial_shader;
+
+  initial_shader = g_resources_lookup_data (path, 0, NULL);
+  gtk_text_buffer_set_text (textbuffer, g_bytes_get_data (initial_shader, NULL), -1);
+  g_bytes_unref (initial_shader);
+
+  run ();
+}
+
+static void
+clear_clicked_cb (GtkWidget *button,
+                    gpointer user_data)
+{
+  gtk_text_buffer_set_text (textbuffer, "", 0);
+}
+
+static void
+close_window (GtkWidget *widget)
+{
+  /* Reset the state */
+  demo_window = NULL;
+  shadertoy = NULL;
+  textbuffer = NULL;
+}
+
+static GtkWidget *
+new_shadertoy (const char *path)
+{
+  GBytes *shader;
+  GtkWidget *toy;
+
+  toy = gtk_shadertoy_new ();
+  shader = g_resources_lookup_data (path, 0, NULL);
+  gtk_shadertoy_set_image_shader (GTK_SHADERTOY (toy),
+                                  g_bytes_get_data (shader, NULL));
+  g_bytes_unref (shader);
+
+  return toy;
+}
+
+static GtkWidget *
+new_button (const char *path)
+{
+  GtkWidget *button, *toy;
+
+  button = gtk_button_new ();
+  g_signal_connect (button, "clicked", G_CALLBACK (load_clicked_cb), (char *)path);
+
+  toy = new_shadertoy (path);
+  gtk_widget_set_size_request (toy, 64, 36);
+  gtk_button_set_child (GTK_BUTTON (button),  toy);
+
+  return button;
+}
+
+static GtkWidget *
+create_shadertoy_window (GtkWidget *do_widget)
+{
+  GtkWidget *window, *box, *hbox, *button, *textview, *sw, *aspect, *centerbox;
+
+  window = gtk_window_new ();
+  gtk_window_set_display (GTK_WINDOW (window),  gtk_widget_get_display (do_widget));
+  gtk_window_set_title (GTK_WINDOW (window), "Shadertoy");
+  gtk_window_set_default_size (GTK_WINDOW (window), 640, 600);
+  g_signal_connect (window, "destroy", G_CALLBACK (close_window), NULL);
+
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, FALSE);
+  gtk_widget_set_margin_start (box, 12);
+  gtk_widget_set_margin_end (box, 12);
+  gtk_widget_set_margin_top (box, 12);
+  gtk_widget_set_margin_bottom (box, 12);
+  gtk_box_set_spacing (GTK_BOX (box), 6);
+  gtk_window_set_child (GTK_WINDOW (window), box);
+
+  aspect = gtk_aspect_frame_new (0.5, 0.5, 1.77777, FALSE);
+  gtk_widget_set_hexpand (aspect, TRUE);
+  gtk_widget_set_vexpand (aspect, TRUE);
+  gtk_box_append (GTK_BOX (box), aspect);
+
+  shadertoy = new_shadertoy ("/shadertoy/alienplanet.glsl");
+  gtk_aspect_frame_set_child (GTK_ASPECT_FRAME (aspect), shadertoy);
+
+  sw = gtk_scrolled_window_new ();
+  gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (sw), 250);
+  gtk_scrolled_window_set_has_frame (GTK_SCROLLED_WINDOW (sw), TRUE);
+  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
+                                  GTK_POLICY_AUTOMATIC,
+                                  GTK_POLICY_AUTOMATIC);
+  gtk_widget_set_hexpand (sw, TRUE);
+  gtk_box_append (GTK_BOX (box), sw);
+
+  textview = gtk_text_view_new ();
+  gtk_text_view_set_monospace (GTK_TEXT_VIEW (textview), TRUE);
+  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), textview);
+
+  textbuffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (textview));
+  gtk_text_buffer_set_text (textbuffer,
+                            gtk_shadertoy_get_image_shader (GTK_SHADERTOY (shadertoy)),
+                            -1);
+
+  centerbox = gtk_center_box_new ();
+  gtk_box_append (GTK_BOX (box), centerbox);
+
+  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE);
+  gtk_box_set_spacing (GTK_BOX (hbox), 6);
+  gtk_center_box_set_start_widget (GTK_CENTER_BOX (centerbox), hbox);
+
+  button = gtk_button_new_from_icon_name ("media-playback-start-symbolic");
+  g_signal_connect (button, "clicked", G_CALLBACK (run_clicked_cb), NULL);
+  gtk_box_append (GTK_BOX (hbox), button);
+
+  button = gtk_button_new_from_icon_name ("edit-clear-all-symbolic");
+  g_signal_connect (button, "clicked", G_CALLBACK (clear_clicked_cb), NULL);
+  gtk_box_append (GTK_BOX (hbox), button);
+
+  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE);
+  gtk_box_set_spacing (GTK_BOX (hbox), 6);
+  gtk_center_box_set_end_widget (GTK_CENTER_BOX (centerbox), hbox);
+
+  button = new_button ("/shadertoy/alienplanet.glsl");
+  gtk_box_append (GTK_BOX (hbox), button);
+
+  button = new_button ("/shadertoy/mandelbrot.glsl");
+  gtk_box_append (GTK_BOX (hbox), button);
+
+  button = new_button ("/shadertoy/neon.glsl");
+  gtk_box_append (GTK_BOX (hbox), button);
+
+  button = new_button ("/shadertoy/cogs.glsl");
+  gtk_box_append (GTK_BOX (hbox), button);
+
+  button = new_button ("/shadertoy/glowingstars.glsl");
+  gtk_box_append (GTK_BOX (hbox), button);
+
+  return window;
+}
+
+GtkWidget *
+do_shadertoy (GtkWidget *do_widget)
+{
+  if (!demo_window)
+    demo_window = create_shadertoy_window (do_widget);
+
+  if (!gtk_widget_get_visible (demo_window))
+    gtk_widget_show (demo_window);
+  else
+    gtk_window_destroy (GTK_WINDOW (demo_window));
+
+  return demo_window;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]