blob: 2de8ecabc41f65e57145cc8ff8389e3df8052959 [file] [log] [blame]
package scrap
import (
"bytes"
"fmt"
"io"
"go.skia.org/infra/go/skerr"
)
// writeInfo contains data for of a single template execution of a scrap node tree.
type writeInfo struct {
imageIDs map[string]int // Map image URLs to image number.
}
// newWriteInfo creates a writeInfo populated with information
// needed to fill a template during execution.
func newWriteInfo(root scrapNode) (writeInfo, error) {
var ret writeInfo
urls, err := root.getImageURLs()
if err != nil {
return ret, skerr.Wrap(err)
}
ret.imageIDs = make(map[string]int)
for i, url := range urls {
ret.imageIDs[url] = i + 1
}
return ret, nil
}
// getNodeImageID will return the image ID that will be used by a shader scrap.
func getNodeImageID(body ScrapBody, info writeInfo) (int, error) {
url, err := getSkSLImageURL(body)
if err != nil {
return 0, skerr.Wrap(err)
}
imgNum, ok := info.imageIDs[url]
if !ok {
return 0, skerr.Fmt("Cannot find index of scrap image url %q", url)
}
return imgNum, nil
}
// loadImagesJS creates the JavaScript promises to load
// all images used by the |root| scrap, and all child scraps.
func loadImagesJS(root scrapNode) (string, error) {
urls, err := root.getImageURLs()
if err != nil {
return "", skerr.Wrap(err)
}
info, err := newWriteInfo(root)
if err != nil {
return "", skerr.Wrap(err)
}
// Write image fetch promises for each unique image in the scrap node tree:
//
// const loadImageX = fetch(url)
// .then((response) => response.arrayBuffer());
var b bytes.Buffer
for _, url := range urls {
imgID, ok := info.imageIDs[url]
if !ok {
return "", skerr.Fmt("Cannot find id for img url %q", url)
}
mustWriteStringf(&b, "const loadImage%d = fetch(\"%s\")\n", imgID, url)
mustWriteStringf(&b, " .then((response) => response.arrayBuffer());\n")
}
mustWriteStringf(&b, "\n")
// Write promise waiter:
//
// Promise.all([loadImage1, ..., loadImageN]).then((values) => {
mustWriteStringf(&b, "Promise.all([")
for i, url := range urls {
imgID, ok := info.imageIDs[url]
if !ok {
return "", skerr.Fmt("Cannot find id for img url %q", url)
}
mustWriteStringf(&b, "loadImage%d", imgID)
if i < len(urls)-1 {
mustWriteStringf(&b, ", ")
}
}
mustWriteStringf(&b, "]).then((values) => {\n")
// Values:
//
// const [imageData1, ..., imageDataN] = values;
mustWriteStringf(&b, " const [")
for i, url := range urls {
imgID, ok := info.imageIDs[url]
if !ok {
return "", skerr.Fmt("Cannot find id for img url %q", url)
}
mustWriteStringf(&b, "imageData%d", imgID)
if i < len(urls)-1 {
mustWriteStringf(&b, ", ")
}
}
mustWriteStringf(&b, "] = values;\n")
// Create shaders:
//
// const imgX = CanvasKit.MakeImageFromEncoded(imageDataX);
// const imgShaderX = imgX.makeShaderCubic(
// CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, 1/3, 1/3);
for i := 1; i <= len(urls); i++ {
mustWriteStringf(&b, " const img%d = CanvasKit.MakeImageFromEncoded(imageData%d);\n", i, i)
mustWriteStringf(&b, " const imgShader%d = img%d.makeShaderCubic(\n", i, i)
mustWriteStringf(&b, " CanvasKit.TileMode.Clamp, CanvasKit.TileMode.Clamp, 1/3, 1/3);\n")
}
return b.String(), nil
}
// writeCreateRuntimeEffects writes JavaScript code need to create the
// runtime effect for the scrap |node| and all child nodes to |w|.
func writeCreateRuntimeEffects(w io.StringWriter, node scrapNode) {
for _, child := range node.Children {
writeCreateRuntimeEffects(w, child)
mustWriteStringf(w, "\n")
}
mustWriteStringf(w, "\n")
if node.Name != "" {
mustWriteStringf(w, " // Shader %q\n", node.Name)
}
mustWriteStringf(w, " const prog%s = `\n", node.Name)
mustWriteStringf(w, indentMultilineString(skslDefaultInputs, 4))
mustWriteStringf(w, "\n")
writeShaderInputDefinitions(w, node, 4)
mustWriteStringf(w, "\n")
mustWriteStringf(w, indentMultilineString(node.Scrap.Body, 4))
mustWriteStringf(w, "\n `;\n")
mustWriteStringf(w, " const effect%s = CanvasKit.RuntimeEffect.Make(prog%s);", node.Name, node.Name)
}
// createRuntimeEffectsJS is the template.Template callback to write
// JavaScript code to create all effects for the given |root| node
// and all child nodes.
func createRuntimeEffectsJS(root scrapNode) (string, error) {
var b bytes.Buffer
writeCreateRuntimeEffects(&b, root)
return b.String(), nil
}
// writeUniformArray writes JavaScript code to define an array with
// all uniform values (stock and custom) for the given |node|.
func writeUniformArray(w io.StringWriter, node scrapNode, imgID int) {
mustWriteStringf(w, " const uniforms%s = [\n", node.Name)
mustWriteStringf(w, " shaderWidth, shaderHeight, 1, // iResolution\n")
mustWriteStringf(w, " iTime, // iTime\n")
mustWriteStringf(w, " mouseDragX, mouseDragY, mouseClickX, mouseClickY, // iMouse\n")
mustWriteStringf(w, " img%d.width(), img%d.height(), 1, // iImageResolution\n", imgID, imgID)
if u := getSkSLCustomUniforms(node.Scrap); u != "" {
mustWriteStringf(w, "%s\n", u)
}
mustWriteStringf(w, " ];\n")
}
// writeChildrenArray writes JavaScript code to define an array with
// all children (stock and custom) for the given |node|.
func writeChildrenArray(w io.StringWriter, node scrapNode, imgID int) {
mustWriteStringf(w, " const children%s = [\n", node.Name)
mustWriteStringf(w, " imgShader%d, // iImage1\n", imgID)
for _, child := range node.Children {
mustWriteStringf(w, " shader%s,\n", child.Name)
}
mustWriteStringf(w, " ];\n")
}
// writeCodeToCreateShader writes JavaScript code to create all shaders
// for a given |node| and all child nodes.
func writeCodeToCreateShader(w io.StringWriter, node scrapNode, info writeInfo) error {
imgID, err := getNodeImageID(node.Scrap, info)
if err != nil {
return skerr.Wrap(err)
}
for _, child := range node.Children {
if err := writeCodeToCreateShader(w, child, info); err != nil {
return skerr.Wrap(err)
}
mustWriteStringf(w, "\n")
}
writeUniformArray(w, node, imgID)
writeChildrenArray(w, node, imgID)
mustWriteStringf(w,
" const shader%s = effect%s.makeShaderWithChildren(uniforms%s, children%s);\n",
node.Name, node.Name, node.Name, node.Name)
mustWriteStringf(w, " if (!shader%s) {\n", node.Name)
mustWriteStringf(w, " throw \"Could not make shader%s\";\n", node.Name)
mustWriteStringf(w, " }")
return nil
}
// createFragmentShadersJS is a Template callback to insert the JavaScript
// code to create all shaders for the |root| node and all child nodes.
func createFragmentShadersJS(root scrapNode) (string, error) {
info, err := newWriteInfo(root)
if err != nil {
return "", skerr.Wrap(err)
}
var b bytes.Buffer
if err := writeCodeToCreateShader(&b, root, info); err != nil {
return "", skerr.Wrap(err)
}
return b.String(), nil
}
// Write JavaScript code to set the CanvasKit.Paint shader for
// the given |node|.
func putShaderOnPaintJS(node scrapNode) string {
return fmt.Sprintf("paint.setShader(shader%s);", node.Name)
}
// Write JavaScript code to delete the shader for the given |node|
// and all child nodes.
func deleteShadersJS(node scrapNode) string {
var b bytes.Buffer
for _, child := range node.Children {
mustWriteStringf(&b, deleteShadersJS(child))
mustWriteStringf(&b, "\n")
}
mustWriteStringf(&b, " shader%s.delete();", node.Name)
return b.String()
}
// The template used to convert a SkSL shader scrap (from shaders.skia.org)
// to JavaScript suitable for direct use in jsfiddle.skia.org.
const skslJavaScript = `const shaderWidth = 512;
const shaderHeight = 512;
{{ loadImagesJS . }}
const surface = CanvasKit.MakeCanvasSurface(canvas.id);
if (!surface) {
throw "Could not make surface";
}
const skcanvas = surface.getCanvas();
const paint = new CanvasKit.Paint();
const startTimeMs = Date.now();
let mouseClickX = 0;
let mouseClickY = 0;
let mouseDragX = 0;
let mouseDragY = 0;
let lastMousePressure = 0;
{{ createRuntimeEffectsJS . }}
function drawFrame(canvas) {
const iTime = (Date.now() - startTimeMs) / 1000;
{{ createFragmentShadersJS . }}
{{ putShaderOnPaintJS . }}
skcanvas.drawPaint(paint);
{{ deleteShadersJS . }}
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
canvas.addEventListener("pointermove", (e) => {
if (e.pressure && !lastMousePressure) {
mouseClickX = e.offsetX;
mouseClickY = e.offsetY;
}
lastMousePressure = e.pressure;
if (!e.pressure) {
return;
}
mouseDragX = e.offsetX;
mouseDragY = e.offsetY;
});
}); // from the Promise.all
`