Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions src/PenSkin.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,15 @@ class PenSkin extends Skin {
*/
dispose () {
this._renderer.removeListener(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
this._renderer.gl.deleteTexture(this._texture);
const gl = this._renderer.gl;
gl.deleteTexture(this._texture);
if (this._framebuffer) gl.deleteFramebuffer(this._framebuffer.framebuffer);
gl.deleteBuffer(this.a_position_glbuffer);
gl.deleteBuffer(this.attribute_glbuffer);
this._texture = null;
this._framebuffer = null;
this.a_position_glbuffer = null;
this.attribute_glbuffer = null;
super.dispose();
}

Expand Down Expand Up @@ -489,6 +496,7 @@ class PenSkin extends Skin {

// tw: store current texture to redraw it later
const oldTexture = this._texture;
const oldFramebuffer = this._framebuffer;

this._texture = twgl.createTexture(
gl,
Expand All @@ -508,21 +516,18 @@ class PenSkin extends Skin {
}
];

if (this._framebuffer) {
// tw: resize framebuffer info doesn't work here, so always make a new framebuffer
// twgl.resizeFramebufferInfo(gl, this._framebuffer, attachments, width, height);
this._framebuffer = twgl.createFramebufferInfo(gl, attachments, width, height);
} else {
this._framebuffer = twgl.createFramebufferInfo(gl, attachments, width, height);
}
// tw: resize framebuffer info doesn't work here, so always make a new framebuffer
this._framebuffer = twgl.createFramebufferInfo(gl, attachments, width, height);

gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);

// tw: preserve old texture when resizing
if (oldTexture) {
this._drawPenTexture(oldTexture);
gl.deleteTexture(oldTexture);
}
if (oldFramebuffer) gl.deleteFramebuffer(oldFramebuffer.framebuffer);

this._silhouettePixels = new Uint8Array(Math.floor(width * height * 4));
this._silhouetteImageData = new ImageData(width, height);
Expand Down
117 changes: 114 additions & 3 deletions src/RenderWebGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ class RenderWebGL extends EventEmitter {
// tw: track id of pen skin
this._penSkinId = null;

// nb: multiple Pen papers can each own a Pen skin.
this._penSkinIds = new Set();

this.useHighQualityRender = false;

this.offscreenTouching = false;
Expand Down Expand Up @@ -336,8 +339,8 @@ class RenderWebGL extends EventEmitter {
this._updateRenderQuality();
}
_updateRenderQuality () {
if (this._penSkinId !== null) {
const skin = this._allSkins[this._penSkinId];
for (const skinId of this._penSkinIds) {
const skin = this._allSkins[skinId];
if (skin) {
if (this.useHighQualityRender) {
skin.setRenderQuality(this.canvas.width / this._nativeSize[0]);
Expand Down Expand Up @@ -596,8 +599,11 @@ class RenderWebGL extends EventEmitter {
this._allSkins[skinId] = newSkin;
// tw: track id of pen skin
this._penSkinId = skinId;
this._penSkinIds.add(skinId);
// tw: high quality pen may have been enabled before the pen skin was created
this._updateRenderQuality();
if (this.useHighQualityRender) {
newSkin.setRenderQuality(this.canvas.width / this._nativeSize[0]);
}
return skinId;
}

Expand Down Expand Up @@ -694,6 +700,10 @@ class RenderWebGL extends EventEmitter {
const oldSkin = this._allSkins[skinId];
oldSkin.dispose();
delete this._allSkins[skinId];
if (this._penSkinIds.delete(skinId) && this._penSkinId === skinId) {
const remainingPenSkinIds = Array.from(this._penSkinIds);
this._penSkinId = remainingPenSkinIds.length ? remainingPenSkinIds[remainingPenSkinIds.length - 1] : null;
}
}

/**
Expand Down Expand Up @@ -1905,6 +1915,107 @@ class RenderWebGL extends EventEmitter {
skin.drawLine(penAttributes, x0, y0, x1, y1);
}

/**
* Draw text onto a pen layer with its first baseline starting at the given Scratch position.
* @param {int} penSkinID - the unique ID of a Pen Skin.
* @param {string} text - text to draw; line breaks create additional lines.
* @param {object} attributes - CSS-compatible font and color attributes.
* @param {number} x - Scratch x coordinate of the first baseline.
* @param {number} y - Scratch y coordinate of the first baseline.
* @returns {Promise<void>} resolves after the text has been drawn
*/
async penText (penSkinID, text, attributes, x, y) {
const font = `${attributes.italic ? 'italic ' : ''}${attributes.weight} ` +
`${attributes.size}px ${attributes.family}`;
if (document.fonts && document.fonts.load) {
await document.fonts.load(font, text);
}

const measurementCanvas = document.createElement('canvas');
const measurementContext = measurementCanvas.getContext('2d');
measurementContext.font = font;
measurementContext.textAlign = attributes.alignment;
let lines;
if (attributes.wordWrap) {
const stageLeft = -this._nativeSize[0] / 2;
const stageRight = this._nativeSize[0] / 2;
let wrapWidth;
if (attributes.alignment === 'right') {
wrapWidth = x - stageLeft;
} else if (attributes.alignment === 'center') {
wrapWidth = Math.min(x - stageLeft, stageRight - x) * 2;
} else {
wrapWidth = stageRight - x;
}
wrapWidth = Math.max(1, Math.min(this._nativeSize[0], wrapWidth));
const measurementProvider = new CanvasMeasurementProvider(measurementContext);
const wrapper = this.createTextWrapper(measurementProvider);
lines = wrapper.wrapText(wrapWidth, text);
} else {
lines = text.split('\n');
}
const metrics = lines.map(line => measurementContext.measureText(line || ' '));
const strokePadding = Math.ceil(attributes.strokeWidth / 2) + 1;
const leftBearing = Math.max(0, ...metrics.map(line => line.actualBoundingBoxLeft || 0));
const rightBearing = Math.max(0, ...metrics.map(line => line.actualBoundingBoxRight || line.width));
const textWidth = Math.max(1, leftBearing + rightBearing);
const ascent = Math.max(1, ...metrics.map(line => line.actualBoundingBoxAscent || attributes.size));
const descent = Math.max(0, ...metrics.map(line => line.actualBoundingBoxDescent || 0));
const lineHeight = Math.max(1, attributes.size * 1.2);
const baselineX = strokePadding + leftBearing;
const baselineY = strokePadding + ascent;

const penSkin = /** @type {PenSkin} */ this._allSkins[penSkinID];
if (!(penSkin instanceof PenSkin)) return;
const quality = penSkin.renderQuality;
const logicalWidth = Math.min(this.maxTextureDimension / quality,
Math.max(1, Math.ceil(textWidth + (strokePadding * 2))));
const logicalHeight = Math.min(this.maxTextureDimension / quality, Math.max(1, Math.ceil(
baselineY + descent + ((lines.length - 1) * lineHeight) + strokePadding
)));

const canvas = document.createElement('canvas');
canvas.width = Math.ceil(logicalWidth * quality);
canvas.height = Math.ceil(logicalHeight * quality);
canvas.reusable = false;
const context = canvas.getContext('2d');
context.scale(quality, quality);
context.font = font;
context.textBaseline = 'alphabetic';
context.textAlign = attributes.alignment;
context.lineJoin = 'round';
context.fillStyle = attributes.color;
context.strokeStyle = attributes.strokeColor;
context.lineWidth = attributes.strokeWidth;
for (let index = 0; index < lines.length; index++) {
const lineY = baselineY + (index * lineHeight);
if (attributes.strokeWidth > 0) context.strokeText(lines[index], baselineX, lineY);
context.fillText(lines[index], baselineX, lineY);
}

const skinID = this._nextSkinId++;
const textSkin = new BitmapSkin(skinID, this);
textSkin.setBitmap(canvas, quality);
this._allSkins[skinID] = textSkin;
const drawableID = this._nextDrawableId++;
const drawable = new Drawable(drawableID, this);
drawable.setHighQuality(this.useHighQualityRender);
drawable.skin = textSkin;
drawable.updatePosition([
x + (logicalWidth / 2) - baselineX,
y + baselineY - (logicalHeight / 2)
]);
this._allDrawables[drawableID] = drawable;

try {
this.penStamp(penSkinID, drawableID);
} finally {
drawable.dispose();
delete this._allDrawables[drawableID];
this.destroySkin(skinID);
}
}

/**
* Stamp a Drawable onto a pen layer.
* @param {int} penSkinID - the unique ID of a Pen Skin.
Expand Down
Loading