import * as scrawl from '../source/scrawl.js'
import { reportSpeed, addImageDragAndDrop, initializeDomInputs } from './utilities.js';
import * as scrawl from '../source/scrawl.js'
import { reportSpeed, addImageDragAndDrop, initializeDomInputs } from './utilities.js';
const canvas = scrawl.findCanvas('mycanvas');
Namespacing boilerplate
const namespace = canvas.name;
const name = (n) => `${namespace}-${n}`;
Magic numbers
const dimension = 600;
Import image from DOM, and create Picture entity using it
scrawl.importDomImage('.flowers');
We need a background image to act as the template on which we will draw
const backgroundImage = scrawl.makePicture({
name: name('background'),
asset: 'iris',
dimensions: [dimension, dimension],
copyDimensions: ['100%', '100%'],
method: 'none',
});
We will use Perlin noise to determine brush stroke length and direction
const noiseAsset = scrawl.makeNoiseAsset({
name: name('my-noise-generator'),
width: dimension,
height: dimension,
noiseEngine: 'improved-perlin',
scale: 80,
});
We’ll code up the painting effect in a RawAsset, which can then be used by Picture entitys, Pattern styles, and filters
const impressionistAsset = scrawl.makeRawAsset({
name: name('pretend-van-gogh'),
userAttributes: [{
lineWidth, lineLengthMultiplier, lineLengthStart, linesToAdd, lineBlend, lineOpacity - some brush attributes that we’ll allow the user to modify in real time.
key: 'lineWidth',
defaultValue: 4,
},{
key: 'lineLengthMultiplier',
defaultValue: 20,
},{
key: 'lineLengthStart',
defaultValue: 5,
},{
key: 'linesToAdd',
defaultValue: 50,
},{
key: 'lineBlend',
defaultValue: 'source-over',
},{
key: 'lineOpacity',
defaultValue: 1,
},{
offsetX, offsetY, rotationMultiplier, rotationStart - some additional brush rotation attributes.
key: 'offsetX',
defaultValue: 0,
},{
key: 'offsetY',
defaultValue: 0,
},{
key: 'rotationMultiplier',
defaultValue: 90,
},{
key: 'rotationStart',
defaultValue: 0,
},{
canvasWidth, canvasHeight - make the RawAsset’s dimensions the same as our canvas base Cell’s dimensions
key: 'canvasWidth',
defaultValue: dimension,
},{
key: 'canvasHeight',
defaultValue: dimension,
},{
background - a handle to our background Picture entity, from which we will be extracting color values
key: 'background',
defaultValue: false,
setter: function (item) {
/** @ts-expect-error */
this.background = item;
/** @ts-expect-error */
this.dirtyBackground = true;
},
},{
noise - a handle to our Noise asset, from which we will be extracting brushstroke direction and length data
key: 'noise',
defaultValue: false,
setter: function (item) {
/** @ts-expect-error */
this.noise = item;
/** @ts-expect-error */
this.dirtyData = true;
},
},{
trigger - we update the RawAsset at the start of each Display cycle by setting its trigger
attribute.
key: 'trigger',
defaultValue: false,
setter: function () {
/** @ts-expect-error */
if (this.dirtyBackground) {
/** @ts-expect-error */
this.dirtyBackground = false;
/** @ts-expect-error */
const { element, engine, canvasWidth, canvasHeight, background } = this;
element.width = canvasWidth;
element.height = canvasHeight;
const { source, copyArray, pasteArray } = background;
if (source && copyArray && pasteArray ) {
Strictly speaking, copyArray and pasteArray are Picture entity internal data structures but that doesn’t stop us using them here.
engine.drawImage(background.source, ...background.copyArray, ...background.pasteArray);
/** @ts-expect-error */
this.backgroundData = engine.getImageData(0, 0, dimension, dimension);
/** @ts-expect-error */
this.dirtyData = true;
}
/** @ts-expect-error */
else this.dirtyBackground = true;
}
/** @ts-expect-error */
else this.dirtyData = true;
},
}],
assetWrapper
is the same as this
when function is declared with the function keyword
updateSource: function (assetWrapper) {
const { engine, noise, backgroundData, lineWidth, lineLengthMultiplier, lineLengthStart, linesToAdd, lineBlend, lineOpacity, offsetX, offsetY, rotationMultiplier, rotationStart } = assetWrapper;
if (noise && backgroundData) {
const { data, width, height } = backgroundData;
const { noiseValues } = noise;
if (noiseValues) {
engine.lineWidth = lineWidth;
engine.lineCap = 'round';
engine.globalCompositeOperation = lineBlend;
engine.globalAlpha = lineOpacity;
let x, y, pos, len, rx, ry, dx, dy, roll, r, g, b, a;
const coord = scrawl.requestCoordinate();
for (let i = 0; i < linesToAdd; i++) {
x = Math.floor(Math.random() * width);
y = Math.floor(Math.random() * height);
len = (noiseValues[y][x] * lineLengthMultiplier) + lineLengthStart;
pos = ((y * width) + x) * 4;
r = data[pos];
g = data[++pos];
b = data[++pos];
a = data[++pos];
engine.strokeStyle = `rgb(${r} ${g} ${b} / ${a/255})`;
rx = (x + offsetX);
if (rx < 0 || rx >= width) {
rx = (rx < 0) ? rx + width : rx - width;
}
ry = (y + offsetY);
if (ry < 0 || ry >= height) {
ry = (ry < 0) ? ry + height : ry - height;
}
roll = (noiseValues[ry][rx] * rotationMultiplier) + rotationStart;
coord.set(len, 0).rotate(roll);
[dx, dy] = coord;
engine.beginPath();
engine.moveTo(x, y);
/** @ts-expect-error */
engine.lineTo(x + dx, y + dy);
engine.stroke();
}
scrawl.releaseCoordinate(coord);
}
}
},
});
impressionistAsset.set({
background: backgroundImage,
noise: noiseAsset,
});
scrawl.makePicture({
name: name('noise-image'),
asset: name('my-noise-generator'),
method: 'none',
});
scrawl.makePicture({
name: name('display-image'),
asset: name('pretend-van-gogh'),
dimensions: [dimension, dimension],
copyDimensions: ['100%', '100%'],
});
Function to display frames-per-second data, and other information relevant to the demo
const report = reportSpeed('#reportmessage');
Create the Display cycle animation
scrawl.makeRender({
name: name('animation'),
target: canvas,
We need to trigger the RawAsset object to update its output at the start of each Display cycle
commence: () => impressionistAsset.set({ trigger: true }),
afterShow: report,
});
addImageDragAndDrop(
canvas,
'#my-image-store',
backgroundImage,
() => {
impressionistAsset.set({
background: backgroundImage,
});
},
);
scrawl.makeUpdater({
event: ['input', 'change'],
origin: '.controlItem',
target: impressionistAsset,
useNativeListener: true,
preventDefault: true,
updates: {
lineBlend: ['lineBlend', 'raw'],
lineLengthMultiplier: ['lineLengthMultiplier', 'round'],
lineLengthStart: ['lineLengthStart', 'round'],
lineWidth: ['lineWidth', 'round'],
linesToAdd: ['linesToAdd', 'round'],
lineOpacity: ['lineOpacity', 'float'],
offsetX: ['offsetX', 'round'],
offsetY: ['offsetY', 'round'],
rotationMultiplier: ['rotationMultiplier', 'round'],
rotationStart: ['rotationStart', 'round'],
},
});
scrawl.makeUpdater({
event: ['input', 'change'],
origin: '#noiseScale',
target: noiseAsset,
useNativeListener: true,
preventDefault: true,
updates: {
noiseScale: ['scale', 'round'],
},
});
Setup form
initializeDomInputs([
['input', 'lineWidth', '4'],
['input', 'lineLengthMultiplier', '20'],
['input', 'lineLengthStart', '5'],
['input', 'linesToAdd', '50'],
['input', 'lineOpacity', '1'],
['input', 'noiseScale', '80'],
['input', 'offsetX', '0'],
['input', 'offsetY', '0'],
['input', 'rotationMultiplier', '90'],
['input', 'rotationStart', '0'],
['select', 'lineBlend', 0],
]);
const videoButton = document.querySelector("#my-record-video-button");
let recording = false;
let myRecorder;
let recordedChunks;
videoButton.addEventListener("click", () => {
recording = !recording;
if (recording) {
videoButton.textContent = "Stop recording";
const stream = canvas.domElement.captureStream(25);
myRecorder = new MediaRecorder(stream, {
mimeType: "video/webm;codecs=vp8"
});
recordedChunks = [];
myRecorder.ondataavailable = (e) => {
if (e.data.size > 0) recordedChunks.push(e.data);
};
myRecorder.start();
}
else {
videoButton.textContent = "Record a video";
myRecorder.stop();
setTimeout(() => {
const blob = new Blob(recordedChunks, { type: "video/webm" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `Scrawl-canvas-art-recording-${Date().slice(4, 24)}.webm`;
a.click();
URL.revokeObjectURL(url);
}, 0);
}
});
console.log(scrawl.library);