| /** |
| * @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('  ').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); |