diff --git a/src/bubble.ts b/src/bubble.ts index 55f9841..cb6c6a0 100644 --- a/src/bubble.ts +++ b/src/bubble.ts @@ -52,11 +52,6 @@ export default class Bubble { this.content = element; this.spec = Bubble.getBubbleSpec(this.content); - - // Just a default placeholder - if (project) { - this.lowerLayer = this.upperLayer = this.handleLayer = project!.activeLayer; - } } private static knownInstances: [HTMLElement, Bubble][] = []; @@ -163,22 +158,34 @@ export default class Bubble { this.handleLayer = layer; } + // Ensures that this bubble has all the required layers and creates them, if necessary + private initializeLayers(): void { + if (!this.lowerLayer) { + this.lowerLayer = new Layer(); // Note that the constructor automatically adds the newly-created layer to the project + } + if (!this.upperLayer) { + this.upperLayer = new Layer(); + } + if (!this.handleLayer) { + this.handleLayer = new Layer(); + } + } public makeShapes() { - // TODO: Cleanup if async version proves unnecessary - // Async version - // this.loadShape(this.getStyle(), (newlyLoadedShape: Shape) => { - // this.wrapShapeAroundDiv(newlyLoadedShape); - // }); // Note: Make sure to use arrow functions to ensure that "this" refers to the right thing. - + this.initializeLayers(); // Because we reuse Bubbles, from one call to convertBubbleJsonToCanvas to another, // a reused bubble might have some tails already, from last time. At one point, as wrapShapeAroundDiv // calls adjustSize, the attempt to adjust the old tails copied parts of them into the new canvas. // To keep things clean we must discard them before we start. + for (let i = 0; i < this.tails.length; ++i) { + // Erase it off the current canvas + this.tails[i].remove(); + } this.tails = []; - var newlyLoadedShape = this.loadShapeSync(this.getStyle()); - this.wrapShapeAroundDiv(newlyLoadedShape); + this.loadShapeAsync(this.getStyle(), (newlyLoadedShape: Shape) => { + this.wrapShapeAroundDiv(newlyLoadedShape); + }); // Note: Make sure to use arrow functions to ensure that "this" refers to the right thing. // Make any tails the bubble should have this.spec.tips.forEach(tail => { @@ -228,11 +235,16 @@ export default class Bubble { } adjustSize() { - var contentWidth = this.content.offsetWidth; - var contentHeight = this.content.offsetHeight; + var contentWidth = -1 + var contentHeight = -1; + + if (this.content) { + contentWidth = this.content.offsetWidth; + contentHeight = this.content.offsetHeight; + } if (contentWidth < 1 || contentHeight < 1) { // Horrible kludge until I can find an event that fires when the object is ready. - window.setTimeout(this.adjustSize, 100); + window.setTimeout(() => { this.adjustSize(); }, 100); return; } var holderWidth = (this.contentHolder as any).size.width; @@ -259,6 +271,10 @@ export default class Bubble { // A callback for after the shape is loaded/place. // Figures out the information for the tail, then draws the shape and tail private drawTailAfterShapePlaced(desiredTip: Tip) { + if (this.spec.style === "none") { + return; + } + const target = new Point(desiredTip.targetX, desiredTip.targetY); const mid = new Point(desiredTip.midpointX, desiredTip.midpointY); const start = new Point(this.content.offsetLeft + this.content.offsetWidth / 2, @@ -288,13 +304,16 @@ export default class Bubble { return svg; } - public loadShape( + private loadShapeAsync( bubbleStyle: string, onShapeLoaded: (s: Shape) => void ) { const svg = Bubble.getShapeSvgString(bubbleStyle); this.lowerLayer.activate(); // Sets this bubble's lowerLayer as the active layer, so that the SVG will be imported into the correct layer. + + // ImportSVG may return asynchronously if the input string is a URL. + // Even though the string we pass contains the svg contents directly (not a URL), when I ran it in Bloom I still got a null shape out as the return value, so best to treat it as async. project!.importSVG(svg, { onLoad: (item: Item) => { onShapeLoaded(item as Shape); @@ -302,16 +321,6 @@ export default class Bubble { }); } - public loadShapeSync( - bubbleStyle: string - ): Shape { - const svg = Bubble.getShapeSvgString(bubbleStyle); - - this.lowerLayer.activate(); // Sets this bubble's lowerLayer as the active layer, so that the SVG will be imported into the correct layer. - const newlyLoadShape = project!.importSVG(svg) as Shape; // I believe Only async if the string you pass in is a URL instead of the literal string containing the contents of the SVG definition - return newlyLoadShape; - } - public drawTail( start: Point, mid: Point, diff --git a/src/comical.ts b/src/comical.ts index 1c48f3b..2d511ff 100644 --- a/src/comical.ts +++ b/src/comical.ts @@ -80,8 +80,8 @@ export default class Comical { // as a connected set so that all overlaps happen properly. // Eventually, we should make distinct sets for each level. // Eventually, we should be able to handle more than one tail per bubble. - var zLevelList = []; - var bubbleList = []; + var zLevelList: number[] = []; + var bubbleList: Bubble[] = []; for (let i = 0; i < elements.snapshotLength; i++) { const element = elements.snapshotItem(i) as HTMLElement; const bubble = Bubble.getInstance(element); diff --git a/src/tail.ts b/src/tail.ts index 2f449db..725b430 100644 --- a/src/tail.ts +++ b/src/tail.ts @@ -103,4 +103,10 @@ export class Tail { this.midHandle.position = newMid; } } + + // Erases the tail from the canvas + remove() { + this.pathFill.remove(); + this.pathstroke.remove(); + } } \ No newline at end of file diff --git a/stories/index.stories.ts b/stories/index.stories.ts index 698cc0a..5751cd4 100644 --- a/stories/index.stories.ts +++ b/stories/index.stories.ts @@ -107,7 +107,7 @@ storiesOf("bubble-edit", module) bubble1.makeShapes(); bubble2.makeShapes(); - + return wrapDiv; }) .add("shout with tail", () => { @@ -138,7 +138,9 @@ storiesOf("bubble-edit", module) tips: [{ targetX: 420, targetY: 400, midpointX: 320, midpointY: 375 }], level: 1 }); - bubble.makeShapes(); + setTimeout(() => { + bubble.makeShapes(); + }, 200); addFinishButton(wrapDiv); return wrapDiv; }) @@ -166,12 +168,17 @@ storiesOf("bubble-edit", module) bubble.setBubbleSpec({ version: "1.0", style: "shout", - tips: [], + tips: [{ targetX: 220, targetY: 250, midpointX: 220, midpointY: 175}], level: 1 }); - bubble.makeShapes(); - addReloadButton(wrapDiv, () => { + setTimeout(() => { + bubble.makeShapes(); + }, 200); + + addButton(wrapDiv, + "Save and Reload", + () => { bubble = Bubble.getInstance(textDiv2); Comical.update(wrapDiv); }); @@ -288,6 +295,67 @@ storiesOf("bubble-edit", module) }, 200); + return wrapDiv; + }) + .add("Change bubble style test", () => { + const wrapDiv = document.createElement("div"); + wrapDiv.style.position = "relative"; + wrapDiv.style.height = "300px"; + const canvas = document.createElement("canvas"); + canvas.height = 300; + canvas.width = 500; + wrapDiv.appendChild(canvas); + setup(canvas); + + const textDiv2 = document.createElement("div"); + textDiv2.innerText = + 'Change the bubble style to None and make sure the tail goes away'; + textDiv2.style.width = "200px"; + textDiv2.style.textAlign = "center"; + textDiv2.style.position = "absolute"; + textDiv2.style.top = "50px"; + textDiv2.style.left = "120px"; + wrapDiv.appendChild(textDiv2); + + let bubble = Bubble.getInstance(textDiv2); + bubble.setBubbleSpec({ + version: "1.0", + style: "shout", + tips: [{ targetX: 220, targetY: 250, midpointX: 220, midpointY: 175}], + level: 1 + }); + + setTimeout(() => { + Comical.update(wrapDiv); + }, 200); + + addButton(wrapDiv, + "None", + () => { + const spec = Bubble.getBubbleSpec(textDiv2); + spec.style = "none"; + bubble.setBubbleSpec(spec); + Comical.update(wrapDiv); + } + ); + addButton(wrapDiv, + "Speech", + () => { + const spec = Bubble.getBubbleSpec(textDiv2); + spec.style = "speech"; + bubble.setBubbleSpec(spec); + Comical.update(wrapDiv); + } + ); + addButton(wrapDiv, + "Shout", + () => { + const spec = Bubble.getBubbleSpec(textDiv2); + spec.style = "shout"; + bubble.setBubbleSpec(spec); + Comical.update(wrapDiv); + } + ); return wrapDiv; }); @@ -331,14 +399,15 @@ function addFinishButton(wrapDiv: HTMLElement): HTMLButtonElement { return button; } -function addReloadButton( +function addButton( wrapDiv: HTMLElement, + buttonText: string, clickHandler: () => void ): HTMLButtonElement { wrapDiv.appendChild(document.createElement("br")); const button = document.createElement("button"); - button.title = "Save and Reload"; - button.innerText = "Save and Reload"; + button.title = buttonText; + button.innerText = buttonText; button.style.zIndex = "100"; wrapDiv.appendChild(button); button.addEventListener("click", () => {