[shaders] Factor out much of the shader handling code into ShaderNode.

- First step towards handling child shaders.
- Also makes the code more testable.
- Includes fixes for uniform-color-sk since this refactor exposed
  a bug where colors didn't round-trip.

Bug: skia:11272
Change-Id: I6b7b7a22eca77a49f767c09b475b8706f3ed6112
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/371156
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/infra-sk/modules/uniform-color-sk/uniform-color-sk.ts b/infra-sk/modules/uniform-color-sk/uniform-color-sk.ts
index 348a766..15d2a15 100644
--- a/infra-sk/modules/uniform-color-sk/uniform-color-sk.ts
+++ b/infra-sk/modules/uniform-color-sk/uniform-color-sk.ts
@@ -20,7 +20,7 @@
   slot: 0,
 };
 
-// Converts the uniform value in the range [0, 1] into a two digit hex string.
+/** Converts the uniform value in the range [0, 1] into a two digit hex string. */
 export const slotToHex = (uniforms: number[], slot: number): string => {
   const s = Math.floor(0.5 + uniforms[slot] * 255).toString(16);
   if (s.length === 1) {
@@ -29,6 +29,14 @@
   return s;
 };
 
+/** Converts the two digit hex into a uniform value in the range [0, 1] */
+export const hexToSlot = (hexDigits: string, uniforms: number[], slot: number): void => {
+  let colorAsFloat = parseInt(hexDigits, 16) / 255;
+  // Truncate to 4 digits of precision.
+  colorAsFloat = Math.floor(colorAsFloat * 10000) / 10000;
+  uniforms[slot] = colorAsFloat;
+};
+
 export class UniformColorSk extends ElementSk implements UniformControl {
   private _uniform: Uniform = defaultUniform;
 
@@ -75,12 +83,9 @@
   applyUniformValues(uniforms: number[]): void {
     // Set all three floats from the color.
     const hex = this.colorInput!.value;
-    const r = parseInt(hex.slice(1, 3), 16) / 255;
-    const g = parseInt(hex.slice(3, 5), 16) / 255;
-    const b = parseInt(hex.slice(5, 7), 16) / 255;
-    uniforms[this.uniform.slot] = r;
-    uniforms[this.uniform.slot + 1] = g;
-    uniforms[this.uniform.slot + 2] = b;
+    hexToSlot(hex.slice(1, 3), uniforms, this.uniform.slot);
+    hexToSlot(hex.slice(3, 5), uniforms, this.uniform.slot + 1);
+    hexToSlot(hex.slice(5, 7), uniforms, this.uniform.slot + 2);
 
     // Set the alpha channel if present.
     if (this.hasAlphaChannel()) {
diff --git a/infra-sk/modules/uniform-color-sk/uniform-color-sk_test.ts b/infra-sk/modules/uniform-color-sk/uniform-color-sk_test.ts
index 63abbee..a9da2af 100644
--- a/infra-sk/modules/uniform-color-sk/uniform-color-sk_test.ts
+++ b/infra-sk/modules/uniform-color-sk/uniform-color-sk_test.ts
@@ -1,7 +1,7 @@
 import './index';
 import { assert } from 'chai';
 import { $$ } from 'common-sk/modules/dom';
-import { slotToHex, UniformColorSk } from './uniform-color-sk';
+import { hexToSlot, slotToHex, UniformColorSk } from './uniform-color-sk';
 import { setUpElementUnderTest } from '../test_util';
 
 describe('uniform-color-sk', () => {
@@ -19,6 +19,29 @@
     });
   });
 
+  describe('hexToSlot', () => {
+    it('converts the two digit hex into a float and stores it in the right slot.', () => {
+      const uniforms = [0, 0, 0];
+      hexToSlot('05', uniforms, 1);
+      assert.deepEqual(uniforms, [0, 0.0196, 0]);
+    });
+  });
+
+  describe('hexToSlot and slotToHex', () => {
+    it('roundtrip correctly from number to hex and back to number', () => {
+      const uniforms = [0, 0, 0];
+      for (let i = 0; i < 255; i++) {
+        // Convert a known hex and place it in slot 1.
+        hexToSlot(i.toString(16), uniforms, 1);
+
+        // Now roundtrip that value out and back into slot 2.
+        const hex = slotToHex(uniforms, 1);
+        hexToSlot(hex, uniforms, 2);
+
+        assert.equal(uniforms[1], uniforms[2]);
+      }
+    });
+  });
 
   describe('uniform-color-sk', () => {
     it('puts values in correct spot in uniforms array', () => {
@@ -36,7 +59,7 @@
       element.applyUniformValues(uniforms);
       assert.deepEqual(
         uniforms,
-        [0, 128 / 255, 144 / 255, 160 / 255, 0],
+        [0, 0.5019, 0.5647, 0.6274, 0],
       );
     });
 
@@ -55,7 +78,7 @@
       element.applyUniformValues(uniforms);
       assert.deepEqual(
         uniforms,
-        [0, 128 / 255, 144 / 255, 160 / 255, 0.5, 0],
+        [0, 0.5019, 0.5647, 0.6274, 0.5, 0],
       );
     });
 
@@ -78,11 +101,10 @@
       element.applyUniformValues(uniforms);
       assert.deepEqual(
         uniforms,
-        [0, 128 / 255, 144 / 255, 160 / 255, 0.5, 0],
+        [0, 0.5019, 0.5647, 0.6274, 0.5, 0],
       );
     });
 
-
     it('throws on invalid uniforms', () => {
       // Uniform is too small to be a color.
       assert.throws(() => {
diff --git a/particles/modules/json/index.ts b/particles/modules/json/index.ts
index e8c484d..ed2d5cb 100644
--- a/particles/modules/json/index.ts
+++ b/particles/modules/json/index.ts
@@ -3,9 +3,14 @@
 export interface SVGMetaData {
 }
 
+export interface ChildShader {
+	UniformName: string;
+	ScrapHashOrName: string;
+}
+
 export interface SKSLMetaData {
 	Uniforms: number[] | null;
-	Children: string[] | null;
+	Children: ChildShader[] | null;
 }
 
 export interface ParticlesMetaData {
diff --git a/scrap/go/scrap/scrap.go b/scrap/go/scrap/scrap.go
index 1378f20..ff32ab1 100644
--- a/scrap/go/scrap/scrap.go
+++ b/scrap/go/scrap/scrap.go
@@ -125,14 +125,21 @@
 	Value float64
 }
 
+// ChildShader is the scrap id of a single child shader along with the name that
+// the uniform should have to access it.
+type ChildShader struct {
+	UniformName     string
+	ScrapHashOrName string
+}
+
 // SKSLMetaData is metadata for SKSL scraps.
 type SKSLMetaData struct {
 	// Uniforms are all the inputs to the shader.
 	Uniforms []float32
 
-	// Child shaders. These values are the hashes of shaders, or, if the value
-	// begins with an "@", they are the name of a named shader.
-	Children []string
+	// Child shaders. A slice because order is important when mapping uniform
+	// names in code to child shaders passed to makeShaderWithChildren.
+	Children []ChildShader
 }
 
 // ParticlesMetaData is metadata for Particle scraps.
diff --git a/shaders/Makefile b/shaders/Makefile
index fc025ce..9746afd 100644
--- a/shaders/Makefile
+++ b/shaders/Makefile
@@ -55,6 +55,12 @@
 	# Run the generated tests just once under Xvfb.
 	xvfb-run --auto-servernum --server-args "-screen 0 1280x1024x24" npx karma start --single-run
 
+.PHONY: testjs-watch
+testjs-watch:
+	# Run the generated tests just once under Xvfb.
+	xvfb-run --auto-servernum --server-args "-screen 0 1280x1024x24" npx karma start --no-single-run
+
+
 get_latest_skia:
 	docker pull gcr.io/skia-public/skia-wasm-release:prod
 
diff --git a/shaders/create-named-scraps.sh b/shaders/create-named-scraps.sh
index 8d11c36..337b3d2 100755
--- a/shaders/create-named-scraps.sh
+++ b/shaders/create-named-scraps.sh
@@ -11,7 +11,7 @@
 # that is complete it should be the canconical way to create named scraps.
 
 # Create a name for each scrap.
-curl --silent -X PUT -d "{\"Hash\": \"7f3550775ad8dd3889eda2ce02429ca0e5234290a5888f3c936394e717547a0c\", \"Description\": \"Shader Inputs\"}" -H 'Content-Type: application/json' http://localhost:9000/_/names/sksl/@inputs
+curl --silent -X PUT -d "{\"Hash\": \"26a447d730ba7afe4df7709b9079fbe2d592136dca60b8ddab7e1b56ea302791\", \"Description\": \"Shader Inputs\"}" -H 'Content-Type: application/json' http://localhost:9000/_/names/sksl/@inputs
 curl --silent -X PUT -d "{\"Hash\": \"f9ae5d2b4d9b4f5f60ae47b46c034bee16290739831f490ad014c3ba93d13e46\", \"Description\": \"Shader Inputs\"}" -H 'Content-Type: application/json' http://localhost:9000/_/names/sksl/@iResolution
 curl --silent -X PUT -d "{\"Hash\": \"c56c6550edb52aff98320153ab05a2bcfa1f300e62a5401e37d16814aaabd618\", \"Description\": \"Shader Inputs\"}" -H 'Content-Type: application/json' http://localhost:9000/_/names/sksl/@iTime
 curl --silent -X PUT -d "{\"Hash\": \"4bca396ca53e90795bda2920a1002a7733149bfe6543eddfa1b803d187581a61\", \"Description\": \"Shader Inputs\"}" -H 'Content-Type: application/json' http://localhost:9000/_/names/sksl/@iMouse
diff --git a/shaders/modules/json/index.ts b/shaders/modules/json/index.ts
index e8c484d..ed2d5cb 100644
--- a/shaders/modules/json/index.ts
+++ b/shaders/modules/json/index.ts
@@ -3,9 +3,14 @@
 export interface SVGMetaData {
 }
 
+export interface ChildShader {
+	UniformName: string;
+	ScrapHashOrName: string;
+}
+
 export interface SKSLMetaData {
 	Uniforms: number[] | null;
-	Children: string[] | null;
+	Children: ChildShader[] | null;
 }
 
 export interface ParticlesMetaData {
diff --git a/shaders/modules/shadernode/index.ts b/shaders/modules/shadernode/index.ts
new file mode 100644
index 0000000..f34ca05
--- /dev/null
+++ b/shaders/modules/shadernode/index.ts
@@ -0,0 +1,334 @@
+/** Defines functions and interfaces for working with a shader.
+ *
+ * Example:
+ *
+ *    const node = new ShaderNode(ck, [imageShader1, imageShader2]);
+ *    node.compile();
+ *    const shader = node.getShader(predefinedUniformValues);
+ */
+import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
+import { Uniform } from '../../../infra-sk/modules/uniform/uniform';
+import {
+  CanvasKit,
+  MallocObj, RuntimeEffect, Shader,
+} from '../../build/canvaskit/canvaskit';
+import { ScrapBody, ScrapID } from '../json';
+
+export const predefinedUniforms = `uniform float3 iResolution;      // Viewport resolution (pixels)
+uniform float  iTime;            // Shader playback time (s)
+uniform float4 iMouse;           // Mouse drag pos=.xy Click pos=.zw (pixels)
+uniform float3 iImageResolution; // iImage1 and iImage2 resolution (pixels)
+uniform shader iImage1;          // An input image (Mandrill).
+uniform shader iImage2;          // An input image (Soccer ball).`;
+
+/** How many of the uniforms listed in predefinedUniforms are of type 'shader'? */
+export const numPredefinedShaderUniforms = predefinedUniforms.match(/^uniform shader/gm)!.length;
+
+/**
+ * Counts the number of uniforms defined in 'predefinedUniforms'. All the
+ * remaining uniforms that start with 'i' will be referred to as "user
+ * uniforms".
+ */
+export const numPredefinedUniforms = predefinedUniforms.match(/^uniform/gm)!.length - numPredefinedShaderUniforms;
+
+/**
+ * The number of lines prefixed to every shader for predefined uniforms. Needed
+ * to properly adjust error line numbers.
+ */
+export const numPredefinedUniformLines = predefinedUniforms.split('\n').length;
+
+/**
+ * Regex that finds lines in shader compiler error messages that mention a line number
+ * and makes that line number available as a capture.
+ */
+export const shaderCompilerErrorRegex = /^error: (\d+)/i;
+
+/** The default shader to fall back to if nothing can be loaded. */
+export const defaultShader = `half4 main(float2 fragCoord) {
+  return vec4(1, 0, 0, 1);
+}`;
+
+const defaultBody: ScrapBody = {
+  Type: 'sksl',
+  Body: defaultShader,
+  SKSLMetaData: {
+    Uniforms: [],
+    Children: [],
+  },
+};
+
+/**
+ * Called ShaderNode because once we support child shaders this will be just one
+ * node in a tree of shaders.
+ */
+export class ShaderNode {
+    /** The scrap ID this shader was last saved as. */
+    private scrapID: string = '';
+
+    /** The saved configuration of the shader. */
+    private body: ScrapBody | null = null;
+
+    /** The shader code compiled. */
+    private effect: RuntimeEffect | null = null;
+
+    private inputImageShaders: Shader[] = [];
+
+    private canvasKit: CanvasKit;
+
+    private uniforms: Uniform[] = [];
+
+    private uniformFloatCount: number = 0;
+
+    /**
+     * Keep a MallocObj around to pass uniforms to the shader to avoid the need
+     * to make copies.
+     */
+    private uniformsMallocObj: MallocObj | null = null;
+
+    /**
+     * Records the code that is currently running, which might differ from the
+     * code in the editor, and the code that was last saved.
+     */
+    private runningCode = defaultShader;
+
+    /**
+     * The current code in the editor, which might differ from the currently
+     * running code, and the code that was last saved.
+     */
+    private _shaderCode = defaultShader;
+
+    private _compileErrorMessage: string = '';
+
+    private _compileErrorLineNumbers: number[] = [];
+
+    /**
+     * These are the uniform values for all the user defined uniforms. They
+     * exclude the predefined uniform values.
+     */
+    private _currentUserUniformValues: number[] = [];
+
+    private _numPredefinedUniformValues: number = 0;
+
+    constructor(canvasKit: CanvasKit, inputImageShaders: Shader[]) {
+      this.canvasKit = canvasKit;
+      if (inputImageShaders.length !== numPredefinedShaderUniforms) {
+        throw new Error(`ShaderNode requires exactly two predefined image shaders, got ${inputImageShaders.length}`);
+      }
+      this.inputImageShaders = inputImageShaders;
+      this.body = defaultBody;
+    }
+
+    /** Loads a scrap from the backend for the given scrap id. */
+    async loadScrap(scrapID: string): Promise<void> {
+      this.scrapID = scrapID;
+      const resp = await fetch(`/_/load/${scrapID}`, {
+        credentials: 'include',
+      });
+      const scrapBody = (await jsonOrThrow(resp)) as ScrapBody;
+      this.setScrap(scrapBody);
+    }
+
+    /** Sets the code and uniforms of a shader to run. */
+    setScrap(scrapBody: ScrapBody): void {
+      this.body = scrapBody;
+      this._shaderCode = this.body.Body;
+      this.currentUserUniformValues = this.body.SKSLMetaData?.Uniforms || [];
+      this.compile();
+    }
+
+    /**
+     * Saves the scrap to the backend returning a Promise that resolves to the
+     * scrap id that it was stored at, or reject on an error.
+     */
+    async saveScrap(): Promise<string> {
+      const body: ScrapBody = {
+        Body: this._shaderCode,
+        Type: 'sksl',
+        SKSLMetaData: {
+          Uniforms: this._currentUserUniformValues,
+          Children: [],
+        },
+      };
+
+      // POST the JSON to /_/upload
+      const resp = await fetch('/_/save/', {
+        credentials: 'include',
+        body: JSON.stringify(body),
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        method: 'POST',
+      });
+      const json = (await jsonOrThrow(resp)) as ScrapID;
+      this.scrapID = json.Hash;
+      this.body = body;
+      return this.scrapID;
+    }
+
+    /**
+     * The possibly edited shader code. If compile has not been called then it
+     * may differ from the current running code as embodied in the getShader()
+     * response.
+     */
+    get shaderCode(): string { return this._shaderCode; }
+
+    set shaderCode(val: string) {
+      this._shaderCode = val;
+    }
+
+    /**
+     * The values that should be used for the used defined uniforms, as opposed
+     * to the predefined uniform values.
+     * */
+    get currentUserUniformValues(): number[] { return this._currentUserUniformValues; }
+
+    set currentUserUniformValues(val: number[]) {
+      this._currentUserUniformValues = val;
+    }
+
+    /**
+     * The full shader compile error message. Only updated on a call to
+     * compile().
+     */
+    get compileErrorMessage(): string {
+      return this._compileErrorMessage;
+    }
+
+    /** The line numbers that errors occurred on. Only updated on a call to
+     * compile(). */
+    get compileErrorLineNumbers(): number[] {
+      return this._compileErrorLineNumbers;
+    }
+
+    /** Compiles the shader code for this node. */
+    compile(): void {
+      this._compileErrorMessage = '';
+      this._compileErrorLineNumbers = [];
+      this.runningCode = this._shaderCode;
+      // eslint-disable-next-line no-unused-expressions
+      this.effect?.delete();
+      this.effect = this.canvasKit!.RuntimeEffect.Make(`${predefinedUniforms}\n${this.runningCode}`, (err) => {
+      // Fix up the line numbers on the error messages, because they are off by
+      // the number of lines we prefixed with the predefined uniforms. The regex
+      // captures the line number so we can replace it with the correct value.
+      // While doing the fix up of the error message we also annotate the
+      // corresponding lines in the CodeMirror editor.
+        err = err.replace(shaderCompilerErrorRegex, (_match, firstRegexCaptureValue): string => {
+          const lineNumber = (+firstRegexCaptureValue - (numPredefinedUniformLines + 1));
+          this._compileErrorLineNumbers.push(lineNumber);
+          return `error: ${lineNumber.toFixed(0)}`;
+        });
+        this._compileErrorMessage = err;
+      });
+
+      // Do some precalculations to avoid bouncing into WASM code too much.
+      this.uniformFloatCount = this.effect?.getUniformFloatCount() || 0;
+      this.buildUniformsFromEffect();
+      this.calcNumPredefinedUniformValues();
+      this.mallocUniformsMallocObj();
+
+      // Fix up currentUserUniformValues if it's the wrong length.
+      const userUniformValuesLength = this.uniformFloatCount - this._numPredefinedUniformValues;
+      if (this.currentUserUniformValues.length !== userUniformValuesLength) {
+        this.currentUserUniformValues = new Array(userUniformValuesLength).fill(0.5);
+      }
+    }
+
+    /** Returns true if this node needs to have its code recompiled. */
+    needsCompile(): boolean {
+      return (this._shaderCode !== this.runningCode);
+    }
+
+    /** Returns true if this node or any child node needs to be saved. */
+    needsSave(): boolean {
+      return (this._shaderCode !== this.body!.Body) || this.userUniformValuesHaveBeenEdited();
+    }
+
+    /** Returns the number of uniforms in the effect. */
+    getUniformCount(): number {
+      return this.uniforms.length;
+    }
+
+    /** Get a description of the uniform at the given index. */
+    getUniform(index: number): Uniform {
+      return this.uniforms[index];
+    }
+
+    /** The total number of floats across all predefined and user uniforms. */
+    getUniformFloatCount(): number {
+      return this.uniformFloatCount;
+    }
+
+    /**
+     * This is really only called once every raf for the shader that has focus,
+     * i.e. that shader that is being displayed on the web UI.
+     */
+    getShader(predefinedUniformsValues: number[]): Shader | null {
+      if (!this.effect) {
+        return null;
+      }
+      const uniformsFloat32Array: Float32Array = this.uniformsMallocObj!.toTypedArray() as Float32Array;
+
+      // Copy in predefined uniforms values.
+      predefinedUniformsValues.forEach((val, index) => { uniformsFloat32Array[index] = val; });
+
+      // Copy in our local edited uniform values to the right spots.
+      this.currentUserUniformValues.forEach((val, index) => { uniformsFloat32Array[index + this._numPredefinedUniformValues] = val; });
+
+      return this.effect!.makeShaderWithChildren(uniformsFloat32Array, false, this.inputImageShaders);
+    }
+
+    get numPredefinedUniformValues(): number {
+      return this._numPredefinedUniformValues;
+    }
+
+    /** The number of floats that are defined by predefined uniforms. */
+    private calcNumPredefinedUniformValues(): void {
+      this._numPredefinedUniformValues = 0;
+      if (!this.effect) {
+        return;
+      }
+      for (let i = 0; i < numPredefinedUniforms; i++) {
+        const u = this.uniforms[i];
+        this._numPredefinedUniformValues += u.rows * u.columns;
+      }
+    }
+
+    /**
+     * Builds this._uniforms from this.effect, which is used to avoid later
+     * repeated calls into WASM.
+     */
+    private buildUniformsFromEffect() {
+      this.uniforms = [];
+      if (!this.effect) {
+        return;
+      }
+      const count = this.effect.getUniformCount();
+      for (let i = 0; i < count; i++) {
+        // Use object spread operator to clone the SkSLUniform and add a name to make a Uniform.
+        this.uniforms.push({ ...this.effect.getUniform(i), name: this.effect.getUniformName(i) });
+      }
+    }
+
+    private mallocUniformsMallocObj(): void {
+      // Copy uniforms into this.uniformsMallocObj, which is kept around to avoid
+      // copying overhead in WASM.
+      if (this.uniformsMallocObj) {
+        this.canvasKit!.Free(this.uniformsMallocObj);
+      }
+      this.uniformsMallocObj = this.canvasKit!.Malloc(Float32Array, this.uniformFloatCount);
+    }
+
+    private userUniformValuesHaveBeenEdited(): boolean {
+      const savedLocalUniformValues = this.body?.SKSLMetaData?.Uniforms || [];
+      if (this._currentUserUniformValues.length !== savedLocalUniformValues.length) {
+        return true;
+      }
+      for (let i = 0; i < this._currentUserUniformValues.length; i++) {
+        if (this._currentUserUniformValues[i] !== savedLocalUniformValues[i]) {
+          return true;
+        }
+      }
+      return false;
+    }
+}
diff --git a/shaders/modules/shadernode/index_test.ts b/shaders/modules/shadernode/index_test.ts
new file mode 100644
index 0000000..e911f4f
--- /dev/null
+++ b/shaders/modules/shadernode/index_test.ts
@@ -0,0 +1,157 @@
+/* eslint-disable dot-notation */
+import './index';
+import { assert } from 'chai';
+import { numPredefinedUniforms, ShaderNode } from './index';
+import { CanvasKit } from '../../build/canvaskit/canvaskit';
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const CanvasKitInit = require('../../build/canvaskit/canvaskit.js');
+
+let canvasKit: CanvasKit | null = null;
+
+const getCanvasKit = async (): Promise<CanvasKit> => {
+  if (canvasKit) {
+    return canvasKit;
+  }
+  canvasKit = await CanvasKitInit({ locateFile: (file: string) => `https://particles.skia.org/dist/${file}` });
+  return canvasKit!;
+};
+
+const createShaderNode = async (): Promise<ShaderNode> => {
+  const ck = await getCanvasKit();
+  const image = ck.MakeImageFromCanvasImageSource(new Image(512, 512));
+  const shader1 = image.makeShaderOptions(ck.TileMode.Clamp, ck.TileMode.Clamp, ck.FilterMode.Linear, ck.MipmapMode.None);
+  const shader2 = image.makeShaderOptions(ck.TileMode.Clamp, ck.TileMode.Clamp, ck.FilterMode.Linear, ck.MipmapMode.None);
+
+  return new ShaderNode(ck, [shader1, shader2]);
+};
+
+describe('ShaderNode', async () => {
+  it('constructor throws when not passed in the correct number of image shaders', async () => {
+    const ck = await getCanvasKit();
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    assert.throws(() => { const node = new ShaderNode(ck, []); });
+  });
+
+  it('constructor builds with a default shader', async () => {
+    const node = await createShaderNode();
+    node.compile();
+
+    // Confirm that all the post-compile pre-calculations are done correctly.
+    assert.equal(node.getUniformCount(), numPredefinedUniforms, 'The default shader doesn\'t have any user uniforms.');
+    assert.equal(node.getUniform(0).name, 'iResolution', 'Confirm the predefined shaders show up in the uniforms.');
+    assert.equal(node.getUniformFloatCount(), node.numPredefinedUniformValues, 'These are equal because the default shader has 0 user uniforms.');
+    assert.isNotNull(node['uniformsMallocObj'], "We Malloc'd");
+    assert.equal(node.numPredefinedUniformValues, 11, 'The number of predefined uniform values is calculated after compile() is called. This value will change if predefinedUniforms is changed.');
+    assert.deepEqual(node.compileErrorLineNumbers, []);
+    assert.equal(node.compileErrorMessage, '');
+  });
+
+  it('updates all values when a new shader is compiled.', async () => {
+    const node = await createShaderNode();
+    node.compile();
+    assert.isNotNull(node.getShader([]));
+
+    // Check our starting values.
+    assert.equal(node.getUniformCount(), numPredefinedUniforms, 'The default shader doesn\'t have any user uniforms.');
+    assert.equal(node.getUniform(0).name, 'iResolution', 'Confirm the predefined shaders show up in the uniforms.');
+    assert.equal(node.getUniformFloatCount(), node.numPredefinedUniformValues, 'These are equal because the default shader has 0 user uniforms.');
+
+    // Set code that has a user uniform, in this case with 4 floats.
+    node.setScrap({
+      Type: 'sksl',
+      Body: `uniform float4 iColorWithAlpha;
+
+      half4 main(float2 fragCoord) {
+        return half4(iColorWithAlpha);
+      }
+      `,
+      SKSLMetaData: {
+        Children: [],
+        Uniforms: [1, 0, 1, 0],
+      },
+    });
+    node.compile();
+    assert.isNotNull(node.getShader([0, 0, 0, 0]));
+
+    // Confirm that all the post-compile pre-calculations are done correctly for the new shader.
+    assert.equal(node.getUniformCount(), numPredefinedUniforms + 1, 'The new shader has 1 user uniform.');
+    assert.equal(node.getUniform(0).name, 'iResolution', 'Confirm the predefined shaders show up in the uniforms.');
+    assert.equal(node.getUniformFloatCount(), node.numPredefinedUniformValues + 4, 'The user uniform contributes 4 floats to the total.');
+  });
+
+  it('correctly indicates when run() needs to be called.', async () => {
+    const node = await createShaderNode();
+    node.compile();
+
+    assert.isFalse(node.needsCompile(), 'Should not need a run immediately after a call to compile().');
+
+    const originalCode = node.shaderCode;
+    node.shaderCode += '\n';
+    assert.isTrue(node.needsCompile(), 'Needs compile when code has changed.');
+    node.shaderCode = originalCode;
+    assert.isFalse(node.needsCompile(), 'No longer needs a compile when change is undone.');
+  });
+
+  it('correctly indicates when save() needs to be called.', async () => {
+    const node = await createShaderNode();
+    node.compile();
+
+    const startingUniformValues = [1, 0, 1, 0];
+    const modifiedUniformValues = [1, 1, 1, 1];
+
+    // Set code that has a user uniform, in this case with 4 floats, because
+    // saving is not only indicated when the code changes, but when the user
+    // uniforms change.
+    node.setScrap({
+      Type: 'sksl',
+      Body: `uniform float4 iColorWithAlpha;
+
+      half4 main(float2 fragCoord) {
+        return half4(iColorWithAlpha);
+      }
+      `,
+      SKSLMetaData: {
+        Children: [],
+        Uniforms: startingUniformValues,
+      },
+    });
+    node.compile();
+
+    // Changing the code means we need to save.
+    const originalCode = node.shaderCode;
+    assert.isFalse(node.needsSave());
+    node.shaderCode += '\n';
+    assert.isTrue(node.needsSave(), 'Needs save if code changed.');
+    node.shaderCode = originalCode;
+    assert.isFalse(node.needsSave(), "Doesn't need save when code restored.");
+
+    // Also changing the user uniform values means we need to save.
+    node.currentUserUniformValues = modifiedUniformValues;
+    assert.isTrue(node.needsSave(), 'Needs save if uniform values changed.');
+    node.currentUserUniformValues = startingUniformValues;
+    assert.isFalse(node.needsSave(), "Doesn't need save if uniform values restored.");
+  });
+
+  it('reports compiler errors', async () => {
+    const node = await createShaderNode();
+    node.compile();
+    node.setScrap({
+      Type: 'sksl',
+      Body: `uniform float4 iColorWithAlpha;
+
+      half4 main(float2 fragCoord) {
+        return half4(iColorWithAlpha) // Missing trailing semicolon.
+      }
+      `,
+      SKSLMetaData: {
+        Children: [],
+        Uniforms: [1, 0, 1, 0],
+      },
+    });
+    node.compile();
+
+    assert.deepEqual(node.compileErrorLineNumbers, [4]);
+    node.compileErrorMessage.startsWith('error: 4:');
+  });
+});
diff --git a/shaders/modules/shaders-app-sk/shaders-app-sk.scss b/shaders/modules/shaders-app-sk/shaders-app-sk.scss
index d4c3c7a..86eb89a 100644
--- a/shaders/modules/shaders-app-sk/shaders-app-sk.scss
+++ b/shaders/modules/shaders-app-sk/shaders-app-sk.scss
@@ -38,6 +38,10 @@
     flex-direction: row;
     flex-wrap: wrap;
 
+    #examples {
+      margin: 0 8px 8px 0;
+    }
+
     canvas {
       margin: 0 16px 16px 0;
     }
@@ -111,11 +115,10 @@
 
   details#shaderinputs {
     display: initial;
-    margin: initial;
     padding: initial;
     list-style: initial;
     box-shadow: initial;
-    margin: 0 0 8px 0;
+    margin: 0 8px 8px 0;
     font-size: 11px;
 
     textarea {
diff --git a/shaders/modules/shaders-app-sk/shaders-app-sk.ts b/shaders/modules/shaders-app-sk/shaders-app-sk.ts
index dcadbc9..377711e 100644
--- a/shaders/modules/shaders-app-sk/shaders-app-sk.ts
+++ b/shaders/modules/shaders-app-sk/shaders-app-sk.ts
@@ -11,16 +11,13 @@
 import CodeMirror from 'codemirror';
 import { $$ } from 'common-sk/modules/dom';
 import { stateReflector } from 'common-sk/modules/stateReflector';
-import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
 import { HintableObject } from 'common-sk/modules/hintable';
 import { isDarkMode } from '../../../infra-sk/modules/theme-chooser-sk/theme-chooser-sk';
 import type {
   CanvasKit,
   Surface,
   Canvas,
-  RuntimeEffect,
   Paint,
-  MallocObj,
   Shader,
 } from '../../build/canvaskit/canvaskit.js';
 
@@ -30,7 +27,6 @@
 import '../../../infra-sk/modules/theme-chooser-sk';
 import { SKIA_VERSION } from '../../build/version';
 import { ElementSk } from '../../../infra-sk/modules/ElementSk/ElementSk';
-import { ScrapBody, ScrapID } from '../json';
 import '../../../infra-sk/modules/uniform-time-sk';
 import '../../../infra-sk/modules/uniform-generic-sk';
 import '../../../infra-sk/modules/uniform-dimensions-sk';
@@ -38,9 +34,12 @@
 import '../../../infra-sk/modules/uniform-mouse-sk';
 import '../../../infra-sk/modules/uniform-color-sk';
 import '../../../infra-sk/modules/uniform-imageresolution-sk';
-import { Uniform, UniformControl } from '../../../infra-sk/modules/uniform/uniform';
+import { UniformControl } from '../../../infra-sk/modules/uniform/uniform';
 import { FPS } from '../fps/fps';
 import { DimensionsChangedEventDetail } from '../../../infra-sk/modules/uniform-dimensions-sk/uniform-dimensions-sk';
+import {
+  defaultShader, numPredefinedUniformLines, predefinedUniforms, ShaderNode,
+} from '../shadernode';
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const CanvasKitInit = require('../../build/canvaskit/canvaskit.js');
@@ -48,41 +47,13 @@
 // This element might be loaded from a different site, and that means we need
 // to be careful about how we construct the URL back to the canvas.wasm file.
 // Start by recording the script origin.
-const scriptOrigin = new URL((document!.currentScript as HTMLScriptElement).src)
-  .origin;
+const scriptOrigin = new URL((document!.currentScript as HTMLScriptElement).src).origin;
 const kitReady = CanvasKitInit({
   locateFile: (file: any) => `${scriptOrigin}/dist/${file}`,
 });
 
 const DEFAULT_SIZE = 512;
 
-const predefinedUniforms = `uniform float3 iResolution;      // Viewport resolution (pixels)
-uniform float  iTime;            // Shader playback time (s)
-uniform float4 iMouse;           // Mouse drag pos=.xy Click pos=.zw (pixels)
-uniform float3 iImageResolution; // iImage1 and iImage2 resolution (pixels)
-uniform shader iImage1;          // An input image (Mandrill).
-uniform shader iImage2;          // An input image (Soccer ball).`;
-
-// How many of the uniforms listed in predefinedUniforms are of type 'shader'?
-const numPredefinedShaderUniforms = predefinedUniforms.match(/^uniform shader/gm)!.length;
-
-// Counts the number of uniforms defined in 'predefinedUniforms'. All the
-// remaining uniforms that start with 'i' will be referred to as "user
-// uniforms".
-const numPredefinedUniforms = predefinedUniforms.match(/^uniform/gm)!.length - numPredefinedShaderUniforms;
-
-// The number of lines prefixed to every shader for predefined uniforms. Needed
-// to properly adjust error line numbers.
-const numPredefinedUniformLines = predefinedUniforms.split('\n').length;
-
-const defaultShader = `half4 main(float2 fragCoord) {
-  return vec4(1, 0, 0, 1);
-}`;
-
-// Regex that finds lines in shader compiler error messages that mention a line number
-// and makes that line number available as a capture.
-const shaderCompilerErrorRegex = /^error: (\d+)/i;
-
 type stateChangedCallback = ()=> void;
 
 // State represents data reflected to/from the URL.
@@ -158,9 +129,9 @@
 const RAF_NOT_RUNNING = -1;
 
 export class ShadersAppSk extends ElementSk {
-  private width: number = 512;
+  private width: number = DEFAULT_SIZE;
 
-  private height: number = 512;
+  private height: number = DEFAULT_SIZE;
 
   private codeMirror: CodeMirror.Editor | null = null;
 
@@ -178,42 +149,17 @@
 
   private inputImageShaders: Shader[] = [];
 
-  private effect: RuntimeEffect | null = null;
-
-  private state: State = defaultState;
-
-  // If not the empty string, this contains the full last shader compiler error
-  // message.
-  private compileErrorMessage: string = '';
+  private shaderNode: ShaderNode | null = null;
 
   // Records the lines that have been marked as having errors. We keep these
   // around so we can clear the error annotations efficiently.
   private compileErrorLines: CodeMirror.TextMarker[] = [];
 
-  // Keep a MallocObj around to pass uniforms to the shader to avoid the need to
-  // make copies.
-  private uniformsMallocObj: MallocObj | null = null;
+  private state: State = defaultState;
 
   // The requestAnimationFrame id if we are running, otherwise we are not running.
   private rafID: number = RAF_NOT_RUNNING;
 
-  // Records the code that we started with, either at startup, or after we've saved.
-  private lastSavedCode = defaultShader;
-
-  // Records the code that is currently running.
-  private runningCode = defaultShader;
-
-  // The current code in the editor.
-  private editedCode = defaultShader;
-
-  // These are the uniform values for all the user defined uniforms. They
-  // exclude the predefined uniform values.
-  private lastSavedUserUniformValues: number[] = [];
-
-  // These are the uniform values for all the user defined uniforms. They
-  // exclude the predefined uniform values.
-  private currentUserUniformValues: number[] = [];
-
   // stateReflector update function.
   private stateChanged: stateChangedCallback | null = null;
 
@@ -225,13 +171,12 @@
 
   private static uniformControls = (ele: ShadersAppSk): TemplateResult[] => {
     const ret: TemplateResult[] = [];
-    const effect = ele.effect;
-    if (!effect) {
+    const node = ele.shaderNode;
+    if (!node) {
       return ret;
     }
-    for (let i = 0; i < effect.getUniformCount(); i++) {
-      // Use object spread operator to clone the SkSLUniform and add a name to make a Uniform.
-      const uniform: Uniform = { ...effect.getUniform(i), name: effect.getUniformName(i) };
+    for (let i = 0; i < node.getUniformCount(); i++) {
+      const uniform = node.getUniform(i);
       if (!uniform.name.startsWith('i')) {
         continue;
       }
@@ -281,7 +226,7 @@
     </header>
     <main>
       <div>
-        <p @click=${ele.fastLoad}>Examples: <a href="/?id=@inputs">Uniforms</a> <a href="/?id=@iResolution">iResolution</a> <a href="/?id=@iTime">iTime</a> <a href="/?id=@iMouse">iMouse</a> <a href="/?id=@iImage">iImage</a></p>
+        <p id=examples @click=${ele.fastLoad}>Examples: <a href="/?id=@inputs">Uniforms</a> <a href="/?id=@iResolution">iResolution</a> <a href="/?id=@iTime">iTime</a> <a href="/?id=@iMouse">iMouse</a> <a href="/?id=@iImage">iImage</a></p>
         <canvas
           id="player"
           width=${ele.width}
@@ -306,9 +251,9 @@
         </div>
         </details>
         <div id="codeEditor"></div>
-        <div ?hidden=${!ele.compileErrorMessage} id="compileErrors">
+        <div ?hidden=${!ele.shaderNode?.compileErrorMessage} id="compileErrors">
           <h3>Errors</h3>
-          <pre>${ele.compileErrorMessage}</pre>
+          <pre>${ele.shaderNode?.compileErrorMessage}</pre>
         </div>
       </div>
       <div id=shaderControls>
@@ -319,14 +264,14 @@
           ${ShadersAppSk.uniformControls(ele)}
         </div>
         <button
-          ?hidden=${ele.editedCode === ele.runningCode}
+          ?hidden=${!ele.shaderNode?.needsCompile()}
           @click=${ele.runClick}
           class=action
         >
           Run
         </button>
         <button
-          ?hidden=${ele.editedCode === ele.lastSavedCode && !ele.userUniformValuesHaveBeenEdited()}
+          ?hidden=${!ele.shaderNode?.needsSave()}
           @click=${ele.saveClick}
           class=action
         >
@@ -388,8 +333,9 @@
           /* getState */ () => (this.state as unknown) as HintableObject,
           /* setState */ (newState: HintableObject) => {
             this.state = (newState as unknown) as State;
+            this.shaderNode = new ShaderNode(this.kit!, this.inputImageShaders);
             if (!this.state.id) {
-              this.startShader(defaultShader);
+              this.run();
             } else {
               this.loadShaderIfNecessary();
             }
@@ -421,14 +367,14 @@
     const newDims = (e as CustomEvent<DimensionsChangedEventDetail>).detail;
     this.width = newDims.width;
     this.height = newDims.height;
-    this.startShader(this.runningCode);
+    this.run();
   }
 
   private monitorIfDevicePixelRatioChanges() {
     // Use matchMedia to detect if the screen resolution changes from the current value.
     // See https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
     const mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
-    matchMedia(mqString).addEventListener('change', () => this.startShader(this.runningCode));
+    matchMedia(mqString).addEventListener('change', () => this.run());
   }
 
   private async loadShaderIfNecessary() {
@@ -436,17 +382,13 @@
       return;
     }
     try {
-      const resp = await fetch(`/_/load/${this.state.id}`, {
-        credentials: 'include',
-      });
-      const json = (await jsonOrThrow(resp)) as ScrapBody;
-      this.lastSavedCode = json.Body;
-      this.startShader(json.Body);
-      if (json.SKSLMetaData && json.SKSLMetaData.Uniforms !== null) {
-        this.setCurrentUserUniformValues(json.SKSLMetaData.Uniforms);
-        // We round trip the uniforms through the controls so we are sure to get an exact match.
-        this.lastSavedUserUniformValues = this.getCurrentUserUniformValues(this.getUniformValuesFromControls());
-      }
+      await this.shaderNode!.loadScrap(this.state.id);
+      this._render();
+
+      const predefinedUniformValues = new Array(this.shaderNode!.numPredefinedUniformValues).fill(0);
+      this.setUniformValuesToControls(predefinedUniformValues.concat(this.shaderNode!.currentUserUniformValues));
+
+      this.run();
     } catch (error) {
       errorMessage(error, 0);
       // Return to the default view.
@@ -455,7 +397,7 @@
     }
   }
 
-  private startShader(shaderCode: string) {
+  private run() {
     this.monitorIfDevicePixelRatioChanges();
     // Cancel any pending drawFrames.
     if (this.rafID !== RAF_NOT_RUNNING) {
@@ -463,9 +405,7 @@
       this.rafID = RAF_NOT_RUNNING;
     }
 
-    this.runningCode = shaderCode;
-    this.editedCode = shaderCode;
-    this.codeMirror!.setValue(shaderCode);
+    this.codeMirror!.setValue(this.shaderNode?.shaderCode || defaultShader);
 
     // eslint-disable-next-line no-unused-expressions
     this.surface?.delete();
@@ -478,30 +418,17 @@
     // the parent surface will do that for us.
     this.canvas = this.surface.getCanvas();
     this.canvasKitContext = this.kit!.currentContext();
-    // eslint-disable-next-line no-unused-expressions
-    this.effect?.delete();
     this.clearAllEditorErrorAnnotations();
-    this.compileErrorMessage = '';
-    this.effect = this.kit!.RuntimeEffect.Make(`${predefinedUniforms}\n${shaderCode}`, (err) => {
-      // Fix up the line numbers on the error messages, because they are off by
-      // the number of lines we prefixed with the predefined uniforms. The regex
-      // captures the line number so we can replace it with the correct value.
-      // While doing the fix up of the error message we also annotate the
-      // corresponding lines in the CodeMirror editor.
-      err = err.replace(shaderCompilerErrorRegex, (_match, firstRegexCaptureValue): string => {
-        const lineNumber = (+firstRegexCaptureValue - (numPredefinedUniformLines + 1));
-        this.setEditorErrorLineAnnotation(lineNumber);
-        return `error: ${lineNumber.toFixed(0)}`;
-      });
-      this.compileErrorMessage = err;
+
+    this.shaderNode!.compile();
+
+    // Set CodeMirror errors if the run failed.
+    this.shaderNode!.compileErrorLineNumbers.forEach((lineNumber: number) => {
+      this.setEditorErrorLineAnnotation(lineNumber);
     });
+
     // Render so the uniform controls get displayed.
     this._render();
-
-    if (!this.effect) {
-      return;
-    }
-
     this.drawFrame();
   }
 
@@ -523,90 +450,53 @@
     ));
   }
 
+  /** Populate the uniforms values from the controls. */
   private getUniformValuesFromControls(): number[] {
-    // Populate the uniforms values from the controls.
-    const uniforms: number[] = new Array(this.effect!.getUniformFloatCount());
+    const uniforms: number[] = new Array(this.shaderNode!.getUniformFloatCount()).fill(0);
     $('#uniformControls > *').forEach((control) => {
       (control as unknown as UniformControl).applyUniformValues(uniforms);
     });
     return uniforms;
   }
 
+  /** Populate the control values from the uniforms. */
   private setUniformValuesToControls(uniforms: number[]): void {
-    // Populate the control values from the uniforms.
     $('#uniformControls > *').forEach((control) => {
       (control as unknown as UniformControl).restoreUniformValues(uniforms);
     });
   }
 
-  private userUniformValuesHaveBeenEdited(): boolean {
-    if (this.currentUserUniformValues.length !== this.lastSavedUserUniformValues.length) {
-      return true;
-    }
-    for (let i = 0; i < this.currentUserUniformValues.length; i++) {
-      if (this.currentUserUniformValues[i] !== this.lastSavedUserUniformValues[i]) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private totalPredefinedUniformValues(): number {
-    let ret = 0;
-    if (!this.effect) {
-      return 0;
-    }
-    for (let i = 0; i < numPredefinedUniforms; i++) {
-      const u = this.effect.getUniform(i);
-      ret += u.rows * u.columns;
-    }
-    return ret;
-  }
-
-  private setCurrentUserUniformValues(userUniformValues: number[]): void {
-    if (this.effect) {
-      const uniforms = this.getUniformValuesFromControls();
-      // Update only the non-predefined uniform values.
-      const begin = this.totalPredefinedUniformValues();
-      for (let i = begin; i < this.effect.getUniformFloatCount(); i++) {
-        uniforms[i] = userUniformValues[i - begin];
-      }
-      this.setUniformValuesToControls(uniforms);
-    }
-  }
-
   private getCurrentUserUniformValues(uniforms: number[]): number[] {
-    const uniformsArray: number[] = [];
-    if (this.effect) {
-      // Return only the non-predefined uniform values.
-      for (let i = this.totalPredefinedUniformValues(); i < this.effect.getUniformFloatCount(); i++) {
-        uniformsArray.push(uniforms[i]);
-      }
+    if (this.shaderNode) {
+      return uniforms.slice(this.shaderNode.numPredefinedUniformValues);
     }
-    return uniformsArray;
+    return [];
+  }
+
+  private getPredefinedUniformValues(uniforms: number[]): number[] {
+    if (this.shaderNode) {
+      return uniforms.slice(0, this.shaderNode.numPredefinedUniformValues);
+    }
+    return [];
   }
 
   private drawFrame() {
     this.fps.raf();
     this.kit!.setCurrentContext(this.canvasKitContext);
     const uniformsArray = this.getUniformValuesFromControls();
-    this.currentUserUniformValues = this.getCurrentUserUniformValues(uniformsArray);
 
-    // Copy uniforms into this.uniformsMallocObj, which is kept around to avoid
-    // copying overhead in WASM.
-    if (!this.uniformsMallocObj) {
-      this.uniformsMallocObj = this.kit!.Malloc(Float32Array, uniformsArray.length);
-    } else if (this.uniformsMallocObj.length !== uniformsArray.length) {
-      this.kit!.Free(this.uniformsMallocObj);
-      this.uniformsMallocObj = this.kit!.Malloc(Float32Array, uniformsArray.length);
+    // TODO(jcgregorio) Change this to be event driven.
+    this.shaderNode!.currentUserUniformValues = this.getCurrentUserUniformValues(uniformsArray);
+
+    const shader = this.shaderNode!.getShader(this.getPredefinedUniformValues(uniformsArray));
+    if (!shader) {
+      errorMessage('Failed to get shader.', 0);
+      return;
     }
-    const uniformsFloat32Array: Float32Array = this.uniformsMallocObj.toTypedArray() as Float32Array;
-    uniformsArray.forEach((val, index) => { uniformsFloat32Array[index] = val; });
-
-    const shader = this.effect!.makeShaderWithChildren(uniformsFloat32Array, false, this.inputImageShaders);
-    this._render();
 
     // Allow uniform controls to update, such as uniform-timer-sk.
+    // TODO(jcgregorio) This is overkill, allow controls to register for a 'raf'
+    //   event if they need to update frequently.
     this._render();
 
     // Draw the shader.
@@ -622,35 +512,13 @@
   }
 
   private async runClick() {
-    this.startShader(this.editedCode);
+    this.run();
     this.saveClick();
   }
 
   private async saveClick() {
-    const userUniformValues = this.getCurrentUserUniformValues(this.getUniformValuesFromControls());
-    const body: ScrapBody = {
-      Body: this.editedCode,
-      Type: 'sksl',
-      SKSLMetaData: {
-        Uniforms: userUniformValues,
-        Children: [],
-      },
-    };
     try {
-      // POST the JSON to /_/upload
-      const resp = await fetch('/_/save/', {
-        credentials: 'include',
-        body: JSON.stringify(body),
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        method: 'POST',
-      });
-      const json = (await jsonOrThrow(resp)) as ScrapID;
-
-      this.state.id = json.Hash;
-      this.lastSavedCode = this.editedCode;
-      this.lastSavedUserUniformValues = userUniformValues;
+      this.state.id = await this.shaderNode!.saveScrap();
       this.stateChanged!();
       this._render();
     } catch (error) {
@@ -659,7 +527,7 @@
   }
 
   private codeChange() {
-    this.editedCode = this.codeMirror!.getValue();
+    this.shaderNode!.shaderCode = this.codeMirror!.getValue();
     this._render();
   }
 
diff --git a/shaders/modules/shaders-app-sk/shaders-app-sk_test.ts b/shaders/modules/shaders-app-sk/shaders-app-sk_test.ts
deleted file mode 100644
index 1b417f8..0000000
--- a/shaders/modules/shaders-app-sk/shaders-app-sk_test.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import './index';
-import { assert } from 'chai';
-import { ShadersAppSk } from './shaders-app-sk';
-
-import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util';
-
-describe('shaders-app-sk', () => {
-  const newInstance = setUpElementUnderTest<ShadersAppSk>('shaders-app-sk');
-
-  let element: ShadersAppSk;
-  beforeEach(() => {
-    element = newInstance();
-  });
-
-  describe('some action', () => {
-    it('some result', () => {
-      assert.isNotNull(element);
-    });
-  });
-});