blob: 74b7d330e9a45118659fc08f6081845cce824ac3 [file] [log] [blame]
/**
* @module modules/shaders-app-sk
* @description <h2><code>shaders-app-sk</code></h2>
*
*/
import { $, $$ } from 'common-sk/modules/dom';
import 'codemirror/mode/clike/clike'; // Syntax highlighting for c-like languages.
import { define } from 'elements-sk/define';
import { html, TemplateResult } from 'lit-html';
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import { errorMessage } from 'elements-sk/errorMessage';
import CodeMirror from 'codemirror';
import { stateReflector } from 'common-sk/modules/stateReflector';
import { HintableObject } from 'common-sk/modules/hintable';
import type {
Canvas,
CanvasKit,
CanvasKitInit as CKInit,
Paint,
Surface,
} from '../../wasm_libs/types/canvaskit';
import { isDarkMode } from '../../../infra-sk/modules/theme-chooser-sk/theme-chooser-sk';
import 'elements-sk/error-toast-sk';
import 'elements-sk/styles/buttons';
import 'elements-sk/styles/select';
import 'elements-sk/icon/edit-icon-sk';
import 'elements-sk/icon/add-icon-sk';
import 'elements-sk/icon/delete-icon-sk';
import '../../../infra-sk/modules/theme-chooser-sk';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import '../../../infra-sk/modules/app-sk';
import '../../../infra-sk/modules/uniform-fps-sk';
import '../../../infra-sk/modules/uniform-time-sk';
import '../../../infra-sk/modules/uniform-generic-sk';
import '../../../infra-sk/modules/uniform-dimensions-sk';
import '../../../infra-sk/modules/uniform-slider-sk';
import '../../../infra-sk/modules/uniform-mouse-sk';
import '../../../infra-sk/modules/uniform-color-sk';
import '../../../infra-sk/modules/uniform-imageresolution-sk';
import { UniformControl } from '../../../infra-sk/modules/uniform/uniform';
import { DimensionsChangedEventDetail } from '../../../infra-sk/modules/uniform-dimensions-sk/uniform-dimensions-sk';
import {
defaultScrapBody,
defaultShader, numPredefinedUniformLines, predefinedUniforms, ShaderNode,
} from '../shadernode';
import { EditChildShaderSk } from '../edit-child-shader-sk/edit-child-shader-sk';
import '../edit-child-shader-sk';
import * as SkSLConstants from '../sksl-constants/sksl-constants';
// It is assumed that canvaskit.js has been loaded and this symbol is available globally.
declare const CanvasKitInit: typeof CKInit;
// It is assumed that this symbol is being provided by a version.js file loaded in before this
// file.
declare const SKIA_VERSION: string;
// 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 kitReady = CanvasKitInit({
locateFile: (file: string) => `${scriptOrigin}/dist/${file}`,
});
const DEFAULT_SIZE = 512;
type stateChangedCallback = ()=> void;
// This works around a TS lint rule included in Bazel rules which requires promises be awaited
// on. We do not necessarily want to await on errorMessage, especially when we tell the error
// message to be up indefinitely.
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
function doNotWait(_: Promise<unknown>) {}
// State represents data reflected to/from the URL.
interface State {
id: string;
}
const defaultState: State = {
id: '@default',
};
// A convenience type that represents HTML Elements which are also UniformControls.
interface UniformControlElement extends Element, UniformControl {}
// Define a new mode and mime-type for SkSL shaders. We follow the shader naming
// covention found in CodeMirror.
CodeMirror.defineMIME('x-shader/x-sksl', {
name: 'clike',
keywords: SkSLConstants.keywords,
types: SkSLConstants.types,
builtin: SkSLConstants.builtins,
blockKeywords: SkSLConstants.blockKeywords,
defKeywords: SkSLConstants.defKeywords,
typeFirstDefinitions: true,
atoms: SkSLConstants.atoms,
modeProps: { fold: ['brace', 'include'] },
});
/** requestAnimationFrame id if requestAnimationFrame is not running. */
const RAF_NOT_RUNNING = -1;
/**
* Each shader example consists of a hash linking it to its corresponding webpages which updates
* the codemirror and canvas. The image source is the source of the thumbnail image (a jpeg) and
* the alt is the alternative text
*/
interface shaderExample {
hash: string;
imageName: string;
}
/** An array of shader examples. Each image name must correspond to the thumbnail name */
const exampleShaders: Array<shaderExample> = [
{
hash: 'de2a4d7d893a7251eb33129ddf9d76ea517901cec960db116a1bbd7832757c1f',
imageName: 'blue-neurons',
},
{
hash: 'ed72577c437c036447372e4c873462fc1bbfc0cb5e9fb0630ab1c07368a0db48',
imageName: 'kaleidoscope',
},
{
hash: 'f9be5248170044ea1b69ee78456eec4be98d3e71a6c61fd0138f6018abda2ac3',
imageName: 'blue-clouds',
},
{
hash: '2bee4488820c3253cd8861e85ce3cab86f482cfddd79cfd240591bf64f7bcc38',
imageName: 'fibonacci-sphere',
},
{
hash: '7cd08fc6b1b23121529c62e31e00b4bd6b49deba9a3904fd01fda3dc5c590050',
imageName: 'mandelbrot',
},
{
hash: '23a360c975c3cb195c89ccdf65ec549e279ce8a959643b447e69cb70614a6eca',
imageName: 'smoke',
},
{
hash: '80c3854680c3e99d71fbe24d86185d5bb20cb047305242f9ecb5aff0f102cf73',
imageName: 'snowfall',
},
{
hash: 'e0ec9ef204763445036d8a157b1b5c8929829c3e1ee0a265ed984aeddc8929e2',
imageName: 'starfield',
},
{
hash: 'e3c8c172e50a69196b2f7712c307ae7099931c3addfc21075ef4ab6aeed11f71',
imageName: 'switch-color',
}];
/**
* A collection of thumbnail snippets that redirect to different shader examples when clicked on.
* Additional examples can be added in example shaders with corresponding webpage hash and name
* which in turn links to its thumbnail.
* @returns the gallery template result
*/
const exampleShadersGalleryTemplate = () => html`
<div class="gallery-container">
<div class="thumbnails"></div>
<div class="scrollbar">
<div class="scrollbar-thumb"></div>
</div>
<div class="slides">
${generateExampleShadersHTML()}
</div>
</div>
`;
/**
* Iterates through example shaders and adds it to an ordered list
*/
const generateExampleShadersHTML = () => html`
<ol class="slides">
${exampleShaders.map((i) => shaderEntry(i))}
</ol>`;
/**
* Formats each shader entry attaching the link, image source, and alternative text
* @param i the shader example
* @returns formated shader exanple entry div
*/
const shaderEntry = (i: shaderExample) => html`
<li class="thumbnails">
<a href=${`https://shaders.skia.org/?id=${i.hash}`}>
<div>
<img src=${cdnImage(i)} alt=${`Clickable thumbnail of ${i.imageName} shader example`}>
</div>
</a>
</li>`;
/**
* Formats the shader example thumbnail url
* @param i a shader example
* @returns the corrrect url for a shader example
*/
const cdnImage = (i: shaderExample) => `https://storage.googleapis.com/skia-world-readable/example-shaders/${i.imageName}-thumbnail.jpeg`;
export class ShadersAppSk extends ElementSk {
private width: number = DEFAULT_SIZE;
private height: number = DEFAULT_SIZE;
private traceCoordX: number = DEFAULT_SIZE / 2;
private traceCoordY: number = DEFAULT_SIZE / 2;
private traceCoordClickMs: number = 0;
private codeMirror: CodeMirror.Editor | null = null;
private canvasEle: HTMLCanvasElement | null = null;
private kit: CanvasKit | null = null;
private surface: Surface | null = null;
private canvas: Canvas | null = null;
private paint: Paint | null = null;
private rootShaderNode: ShaderNode | null = null;
private currentNode: 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[] = [];
private state: State = defaultState;
/** The requestAnimationFrame id if we are running, otherwise we are not running. */
private rafID: number = RAF_NOT_RUNNING;
/** stateReflector update function. */
private stateChanged: stateChangedCallback | null = null;
private uniformControlsNeedingRAF: UniformControl[] = [];
/**
* Calculated when we render, it count the number of controls that are for
* predefined uniforms, as opposed to user uniform controls.
*/
private numPredefinedUniformControls: number = 0;
private editChildShaderControl: EditChildShaderSk | null = null;
constructor() {
super(ShadersAppSk.template);
}
private static deleteButton = (ele: ShadersAppSk, parentNode: ShaderNode | null, node: ShaderNode, index: number): TemplateResult => {
if (ele.rootShaderNode === node || parentNode === null) {
return html``;
}
return html`
<button
class=deleteButton
title="Delete child shader."
@click=${(e: Event) => ele.removeChildShader(e, parentNode, index)}>
<delete-icon-sk></delete-icon-sk>
</button>
`;
}
private static editButton = (ele: ShadersAppSk, parentNode: ShaderNode | null, node: ShaderNode, index: number): TemplateResult => {
if (ele.rootShaderNode === node || parentNode === null) {
return html``;
}
return html`
<button
class=editButton
title="Edit child shader uniform name."
@click=${(e: Event) => ele.editChildShader(e, parentNode, index)}>
<edit-icon-sk></edit-icon-sk>
</button>
`;
}
private static displayShaderTreeImpl = (ele: ShadersAppSk, parentNode: ShaderNode | null, node: ShaderNode, depth: number = 0, name: string = '/', childIndex: number = 0): TemplateResult[] => {
let ret: TemplateResult[] = [];
// Prepend some fixed width spaces based on the depth so we get a nested
// directory type of layout. See https://en.wikipedia.org/wiki/Figure_space.
const prefix = new Array(depth).fill('&numsp;&numsp;').join('');
ret.push(html`
<p
class="childShader"
@click=${() => ele.childShaderClick(node)}>
<span>
${unsafeHTML(prefix)}
<span class=linkish>${name}</span>
${(ele.rootShaderNode!.children.length > 0 && ele.currentNode === node) ? '*' : ''}
</span>
<span>
${ShadersAppSk.deleteButton(ele, parentNode, node, childIndex)}
${ShadersAppSk.editButton(ele, parentNode, node, childIndex)}
<button
class=addButton
title="Append a new child shader."
@click=${(e: Event) => ele.appendChildShader(e, node)}>
<add-icon-sk></add-icon-sk>
</button>
</span>
</p>`);
node.children.forEach((childNode, index) => {
ret = ret.concat(ShadersAppSk.displayShaderTreeImpl(ele, node, childNode, depth + 1, node.getChildShaderUniformName(index), index));
});
return ret;
}
private static displayShaderTree = (ele: ShadersAppSk): TemplateResult[] => {
if (!ele.rootShaderNode) {
return [
html``,
];
}
return ShadersAppSk.displayShaderTreeImpl(ele, null, ele.rootShaderNode);
}
// TODO (anjulij): add this back to example shaders
private static uniformControls = (ele: ShadersAppSk): TemplateResult[] => {
const ret: TemplateResult[] = [
html`<uniform-fps-sk></uniform-fps-sk>`, // Always start with the fps control.
];
ele.numPredefinedUniformControls = 1;
const node = ele.currentNode;
if (!node) {
return ret;
}
for (let i = 0; i < node.getUniformCount(); i++) {
const uniform = node.getUniform(i);
if (!uniform.name.startsWith('i')) {
continue;
}
switch (uniform.name) {
case 'iTime':
ele.numPredefinedUniformControls++;
ret.push(html`<uniform-time-sk .uniform=${uniform}></uniform-time-sk>`);
break;
case 'iMouse':
ele.numPredefinedUniformControls++;
ret.push(html`<uniform-mouse-sk .uniform=${uniform} .elementToMonitor=${ele.canvasEle}></uniform-mouse-sk>`);
break;
case 'iResolution':
ele.numPredefinedUniformControls++;
ret.push(html`
<uniform-dimensions-sk
.uniform=${uniform}
@dimensions-changed=${ele.dimensionsChanged}
></uniform-dimensions-sk>`);
break;
case 'iImageResolution':
// No-op. This is no longer handled via uniform control, the
// dimensions are handed directly into the ShaderNode from the image
// measurements.
break;
default:
if (uniform.name.toLowerCase().indexOf('color') !== -1) {
ret.push(html`<uniform-color-sk .uniform=${uniform}></uniform-color-sk>`);
} else if (uniform.rows === 1 && uniform.columns === 1) {
ret.push(html`<uniform-slider-sk .uniform=${uniform}></uniform-slider-sk>`);
} else {
ret.push(html`<uniform-generic-sk .uniform=${uniform}></uniform-generic-sk>`);
}
break;
}
}
return ret;
}
private static template = (ele: ShadersAppSk) => html`
<app-sk>
<header>
<a href="/">SkSL Shaders</a>
<span>
<a
id="githash"
href="https://skia.googlesource.com/skia/+show/${SKIA_VERSION}"
>
${SKIA_VERSION.slice(0, 7)}
</a>
<theme-chooser-sk class="theme-chooser"></theme-chooser-sk>
</span>
</header>
<main>
<div>
<div class="example-gallery-and-canvas-wrapper">
<div>
${exampleShadersGalleryTemplate()}
</div>
<canvas
id="player"
width=${ele.width}
height=${ele.height}
>
Your browser does not support the canvas tag.
</canvas>
</div>
<div>
${ShadersAppSk.displayShaderTree(ele)}
</div>
</div>
<div>
<details id=shaderinputs>
<summary>Shader Inputs</summary>
<textarea rows=${numPredefinedUniformLines} cols=75 readonly id="predefinedShaderInputs">${predefinedUniforms}</textarea>
<div id=imageSources>
<figure>
${ele.currentNode?.inputImageElement}
<figcaption>iImage1</figcaption>
</figure>
<details id=image_edit>
<summary><edit-icon-sk></edit-icon-sk></summary>
<div id=image_edit_dialog>
<label for=image_url>
Change the URL used for the source image.
</label>
<div>
<input type=url id=image_url placeholder="URL of image to use." .value="${ele.currentNode?.getSafeImageURL() || ''}">
<button @click=${ele.imageURLChanged}>Use</button>
</div>
<label for=image_upload>
Or upload an image to <em>temporarily</em> try as a source for the shader. Uploaded images are not saved.
</label>
<div>
<input @change=${ele.imageUploaded} type=file id=image_upload accept="image/*">
</div>
</div>
</details>
</div>
</details>
<textarea style="display: ${ele.currentNode?.children.length ? 'block' : 'none'}" rows=${ele.currentNode?.children.length || 0} cols=75>${ele.currentNode?.getChildShaderUniforms() || ''}</textarea>
<div id="codeEditor"></div>
<div ?hidden=${!ele.currentNode?.compileErrorMessage} id="compileErrors">
<h3>Errors</h3>
<pre>${ele.currentNode?.compileErrorMessage}</pre>
</div>
</div>
<div id=shaderControls>
<div id=uniformControls @input=${ele.uniformControlsChange} @change=${ele.uniformControlsChange}>
${ShadersAppSk.uniformControls(ele)}
</div>
<button
?hidden=${!ele.rootShaderNode?.needsCompile()}
@click=${ele.runClick}
class=action
>
Run
</button>
<button
?hidden=${!ele.rootShaderNode?.needsSave()}
@click=${ele.saveClick}
class=action
>
Save
</button>
<button
?hidden=${ele.currentNode?.compileErrorMessage || ele.rootShaderNode?.needsCompile()}
@click=${ele.createDebugTrace}
class=action
>
Debug
</button>
</div>
</main>
<footer>
<edit-child-shader-sk></edit-child-shader-sk>
<error-toast-sk></error-toast-sk>
</footer>
</app-sk>
`;
/** Returns the CodeMirror theme based on the state of the page's darkmode.
*
* For this to work the associated CSS themes must be loaded. See
* shaders-app-sk.scss.
*/
private static themeFromCurrentMode = () => (isDarkMode() ? 'ambiance' : 'base16-light');
connectedCallback(): void {
super.connectedCallback();
this._render();
this.canvasEle = $$<HTMLCanvasElement>('#player', this);
this.codeMirror = CodeMirror($$<HTMLDivElement>('#codeEditor', this)!, {
lineNumbers: true,
mode: 'x-shader/x-sksl',
theme: ShadersAppSk.themeFromCurrentMode(),
viewportMargin: Infinity,
scrollbarStyle: 'native',
});
this.codeMirror.on('change', () => this.codeChange());
this.editChildShaderControl = $$('edit-child-shader-sk', this);
// Listen for theme changes.
document.addEventListener('theme-chooser-toggle', () => {
this.codeMirror!.setOption('theme', ShadersAppSk.themeFromCurrentMode());
});
// Listen for clicks on the canvas, to update the debug trace coordinate.
this.canvasEle!.addEventListener('click', (e: MouseEvent) => {
this.traceCoordX = Math.floor(e.offsetX);
this.traceCoordY = Math.floor(e.offsetY);
this.traceCoordClickMs = Date.now();
});
// Continue the setup once CanvasKit WASM has loaded.
kitReady.then(async (ck: CanvasKit) => {
this.kit = ck;
this.paint = new this.kit.Paint();
try {
this.stateChanged = stateReflector(
/* getState */ () => (this.state as unknown) as HintableObject,
/* setState */ async (newState: HintableObject) => {
this.state = (newState as unknown) as State;
this.rootShaderNode = new ShaderNode(this.kit!);
this.currentNode = this.rootShaderNode;
if (!this.state.id) {
await this.rootShaderNode.setScrap(defaultScrapBody);
this.run();
} else {
await this.loadShaderIfNecessary();
}
},
);
} catch (error) {
doNotWait(errorMessage(error, 0));
}
});
}
private dimensionsChanged(e: Event) {
const newDims = (e as CustomEvent<DimensionsChangedEventDetail>).detail;
this.width = newDims.width;
this.height = newDims.height;
this.traceCoordX = Math.floor(this.width / 2);
this.traceCoordY = Math.floor(this.height / 2);
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.run());
}
private async loadShaderIfNecessary() {
if (!this.state.id) {
return;
}
try {
await this.rootShaderNode!.loadScrap(this.state.id);
this._render();
this.setUniformValuesToControls();
this.run();
} catch (error) {
doNotWait(errorMessage(error, 0));
// Return to the default view.
this.state = Object.assign({}, defaultState);
this.stateChanged!();
}
}
private run() {
this.monitorIfDevicePixelRatioChanges();
// Cancel any pending drawFrames.
if (this.rafID !== RAF_NOT_RUNNING) {
cancelAnimationFrame(this.rafID);
this.rafID = RAF_NOT_RUNNING;
}
// TODO(jcgregorio) In the long run maybe store scrollInfo and cursorPos
// temporarily for each child shader so we restore the right view as the
// user moves between child shaders?
// Save the scroll info and the cursor position before updating the code.
const scrollInfo = this.codeMirror!.getScrollInfo();
const cursorPos = this.codeMirror!.getCursor();
// Set code.
this.codeMirror!.setValue(this.currentNode?.shaderCode || defaultShader);
// Restore scroll info and cursor position.
this.codeMirror!.setCursor(cursorPos);
// Oddly CodeMirror TS doesn't have a Type defined for this shape.
const scrollPosition = {
left: scrollInfo.left,
top: scrollInfo.top,
right: scrollInfo.left + scrollInfo.width,
bottom: scrollInfo.top + scrollInfo.height,
};
this.codeMirror!.scrollIntoView(scrollPosition);
// eslint-disable-next-line no-unused-expressions
this.surface?.delete();
this.surface = this.kit!.MakeCanvasSurface(this.canvasEle!);
if (!this.surface) {
doNotWait(errorMessage('Could not make Surface.', 0));
return;
}
// We don't need to call .delete() on the canvas because
// the parent surface will do that for us.
this.canvas = this.surface.getCanvas();
this.clearAllEditorErrorAnnotations();
this.rootShaderNode!.compile();
// Set CodeMirror errors if the run failed.
this.currentNode!.compileErrorLineNumbers.forEach((lineNumber: number) => {
this.setEditorErrorLineAnnotation(lineNumber);
});
// Render so the uniform controls get displayed.
this._render();
this.drawFrame();
}
private clearAllEditorErrorAnnotations(): void {
// eslint-disable-next-line no-unused-expressions
this.compileErrorLines?.forEach((textMarker) => {
textMarker.clear();
});
}
private setEditorErrorLineAnnotation(lineNumber: number): void {
// Set the class of that line to 'cm-error'.
this.compileErrorLines.push(this.codeMirror!.markText(
{ line: lineNumber - 1, ch: 0 },
{ line: lineNumber - 1, ch: 9999 }, // Some large number for the character offset.
{
className: 'cm-error', // See the ambiance.css file in CodeMirror for the class name.
},
));
}
/** Query the uniforms from the controls defined by the user. These uniforms will be packed into
* the beginning of the uniform buffer and be the same for all shaders. By being the same, this
* will allow a parent shader to be in sync with its children shaders.
*/
private getPredefinedUniformValuesFromControls(): number[] {
if (!this.currentNode) {
return [];
}
const uniforms: number[] = new Array(this.currentNode.numPredefinedUniformValues).fill(0);
$<UniformControlElement>('#uniformControls > *')
.slice(0, this.numPredefinedUniformControls) // stop after the predefined controls
.forEach((control: UniformControl) => {
control.applyUniformValues(uniforms);
});
return uniforms;
}
/** Query the uniforms from the controls defined by the user. These uniforms will be packed
* into memory after the predefined uniforms (e.g. time, resolution).
*/
private getUserUniformValuesFromControls(): number[] {
if (!this.currentNode) {
return [];
}
// Make a full array because the application of uniform values are implemented to write to
// specific indexes and we need those later indexes to exist.
const uniforms: number[] = new Array(this.currentNode.getUniformFloatCount()).fill(0);
$<UniformControlElement>('#uniformControls > *')
.slice(this.numPredefinedUniformControls) // start after the predefined controls
.forEach((control: UniformControl) => {
control.applyUniformValues(uniforms);
});
// Slice off the uniform values belonging to indices for the predefined uniforms. These
// should all be zero anyway.
return uniforms.slice(this.currentNode.numPredefinedUniformValues);
}
/** Populate the control values from the uniforms. */
private setUniformValuesToControls(): void {
const predefinedUniformValues = new Array(this.currentNode!.numPredefinedUniformValues).fill(0);
const uniforms = predefinedUniformValues.concat(this.currentNode!.currentUserUniformValues);
$<UniformControlElement>('#uniformControls > *').forEach((control: UniformControl) => {
control.restoreUniformValues(uniforms);
});
this.findAllUniformControlsThatNeedRAF();
}
private findAllUniformControlsThatNeedRAF(): void {
this.uniformControlsNeedingRAF = [];
$<UniformControlElement>('#uniformControls > *').forEach((control: UniformControl) => {
if (control.needsRAF()) {
this.uniformControlsNeedingRAF.push(control);
}
});
}
private uniformControlsChange(): void {
this.currentNode!.currentUserUniformValues = this.getUserUniformValuesFromControls();
this._render();
}
private drawFrame(): void {
const shader = this.currentNode!.getShader(this.getPredefinedUniformValuesFromControls());
if (!shader || !this.kit) {
return;
}
// Allow uniform controls to update, such as uniform-timer-sk.
this.uniformControlsNeedingRAF.forEach((element) => {
element.onRAF();
});
// Draw the shader.
this.canvas!.clear(this.kit.BLACK);
this.paint!.setShader(shader);
const rect = this.kit.XYWHRect(0, 0, this.width, this.height);
this.canvas!.drawRect(rect, this.paint!);
// If the user recently clicked on the canvas to set the trace coordinate, circle it.
// The circle pulses three times; each pulse lasts 200ms.
const blinkMs: number = Date.now() - this.traceCoordClickMs;
if (blinkMs < 600) {
const phase = ((blinkMs % 200) / 200); // increases from 0 to 1 over each pulse
const paint: Paint = new this.kit.Paint();
paint.setAntiAlias(true);
paint.setStyle(this.kit.PaintStyle.Stroke);
paint.setStrokeWidth(2.0);
const opacity: number = 1.0 - phase;
paint.setColor(this.kit.Color4f(1, 0, 0, opacity));
const size: number = 10 + (phase * 5);
const ovalFrame = this.kit.XYWHRect(this.traceCoordX - size, this.traceCoordY - size,
2 * size, 2 * size);
this.canvas!.drawOval(ovalFrame, paint);
paint.delete();
}
this.surface!.flush();
shader.delete();
this.rafID = requestAnimationFrame(() => {
this.drawFrame();
});
}
private createDebugTrace(): void {
const shader = this.currentNode!.getShader(this.getPredefinedUniformValuesFromControls());
if (!shader || !this.kit) {
return;
}
// Debug traces require a software surface.
const surface: Surface = this.kit.MakeSurface(this.width, this.height)!;
if (surface) {
const canvas: Canvas = surface.getCanvas();
const paint: Paint = new this.kit.Paint();
const traced = this.kit.RuntimeEffect.MakeTraced(shader, this.traceCoordX, this.traceCoordY);
paint.setShader(traced.shader);
// Clip to a tight rectangle around the trace coordinate to reduce draw time.
const tightClip = this.kit.XYWHRect(this.traceCoordX - 2, this.traceCoordY - 2, 5, 5);
canvas.clipRect(tightClip, this.kit.ClipOp.Intersect, /* doAntiAlias= */ false);
const rect = this.kit.XYWHRect(0, 0, this.width, this.height);
canvas.drawRect(rect, paint);
const traceJSON: string = traced.debugTrace.writeTrace();
traced.shader.delete();
traced.debugTrace.delete();
paint.delete();
surface.delete();
// Write our trace JSON to HTML local storage, where the debugger can see it.
localStorage.setItem('sksl-debug-trace', traceJSON);
// Open the debugger in a separate tab, pointing it at our local storage buffer.
window.open('/debug?local-storage', 'sksl-debug-target');
}
shader.delete();
}
private async runClick() {
this.run();
await this.saveClick();
}
private async saveClick() {
try {
this.state.id = await this.rootShaderNode!.saveScrap();
this.stateChanged!();
this._render();
} catch (error) {
doNotWait(errorMessage(`${error}`, 0));
}
}
private imageUploaded(e: Event) {
const input = e.target as HTMLInputElement;
if (!input.files?.length) {
return;
}
const file = input.files.item(0)!;
this.setCurrentImageURL(URL.createObjectURL(file));
}
private imageURLChanged(): void {
const input = $$<HTMLInputElement>('#image_url', this)!;
if (!input.value) {
return;
}
this.setCurrentImageURL(input.value);
}
private codeChange() {
if (!this.currentNode) {
return;
}
this.currentNode.shaderCode = this.codeMirror!.getValue();
this._render();
}
private async appendChildShader(e: Event, node: ShaderNode) {
e.stopPropagation();
try {
await node.appendNewChildShader();
this._render();
await this.runClick();
} catch (error) {
doNotWait(errorMessage(error));
}
}
private async removeChildShader(e: Event, parentNode: ShaderNode, index: number) {
e.stopPropagation();
// We could write a bunch of complicated logic to track which current shader
// is selected and restore that correctly on delete, or we can just always
// shove the focus back to rootShaderNode which will always work, so we do
// the latter.
this.childShaderClick(this.rootShaderNode!);
parentNode.removeChildShader(index);
this._render();
await this.runClick();
}
private async editChildShader(e: Event, parentNode: ShaderNode, index: number) {
e.stopPropagation();
const editedChildShader = await this.editChildShaderControl!.show(parentNode.getChildShader(index));
if (!editedChildShader) {
return;
}
await parentNode.setChildShaderUniformName(index, editedChildShader.UniformName);
this._render();
await this.runClick();
}
private childShaderClick(node: ShaderNode) {
this.currentNode = node;
this.codeMirror!.setValue(this.currentNode?.shaderCode || defaultShader);
this._render();
this.setUniformValuesToControls();
}
/**
* Load example by changing state rather than actually following the links.
*/
private async fastLoad(e: Event): Promise<void> {
const ele = (e.target as HTMLLinkElement);
if (ele.tagName !== 'A') {
return;
}
e.preventDefault();
// When switching shaders clear the file upload.
$$<HTMLInputElement>('#image_upload')!.value = '';
const id = new URL(ele.href).searchParams.get('id') || '';
this.state.id = id;
this.stateChanged!();
await this.loadShaderIfNecessary();
}
private setCurrentImageURL(url: string): void {
const oldURL = this.currentNode!.getCurrentImageURL();
// Release unused memory.
if (oldURL.startsWith('blob:')) {
URL.revokeObjectURL(oldURL);
}
this.currentNode!.setCurrentImageURL(url).then(() => this._render());
}
}
define('shaders-app-sk', ShadersAppSk);