diff --git a/src/bubble.ts b/src/bubble.ts index 1f74bee..5f18fd1 100644 --- a/src/bubble.ts +++ b/src/bubble.ts @@ -1,4 +1,4 @@ -import { Path, Point, Color, ToolEvent, Item, Shape, project } from "paper"; +import { Path, Point, Color, ToolEvent, Item, Shape, project, Layer } from "paper"; import { BubbleSpec, Tip } from "bubbleSpec"; import Comical from "./comical"; import { Tail } from "./tail"; @@ -25,6 +25,10 @@ export default class Bubble { private innerShape: Shape; private tails: Tail[] = []; + private lowerLayer: Layer; + private upperLayer: Layer; + private handleLayer: Layer; + // Don't use new() to create Bubble elements. Use getInstance() instead. // The reason why is because if multiple Bubble objects get created which correspond to the same element, they will have different member variables // (e.g. different spec variables). If multiple objects are allowed, then a lot more flushing changes and re-reading the HTML will be required to keep them in sync. @@ -33,6 +37,11 @@ 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][] = []; @@ -114,10 +123,41 @@ export default class Bubble { this.persistBubbleSpec(); } + public setLayers(newLowerLayer: Layer, newUpperLayer: Layer, newHandleLayer: Layer): void { + this.setLowerLayer(newLowerLayer); + this.setUpperLayer(newUpperLayer); + this.setHandleLayer(newHandleLayer); + } + + // Sets the value of lowerLayer. The "outline" shapes are drawn in the lower layer. + public setLowerLayer(layer: Layer): void { + this.lowerLayer = layer; + } + + public getUpperLayer(): Layer { + return this.upperLayer; + } + + // Sets the value of upperLayer. The "fill" shapes are drawn in the upper layer. + public setUpperLayer(layer: Layer): void { + this.upperLayer = layer; + } + + // The layer containing the tip and midpoint curve handles + public setHandleLayer(layer: Layer): void { + this.handleLayer = layer; + } + + public makeShapes() { - Bubble.loadShape(this.getStyle(), (newlyLoadedShape: Shape) => { - this.wrapShapeAroundDiv(newlyLoadedShape); - }); // Note: Make sure to use arrow functions to ensure that "this" refers to the right thing. + // 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. + + var newlyLoadedShape = this.loadShapeSync(this.getStyle()); + this.wrapShapeAroundDiv(newlyLoadedShape); } private wrapShapeAroundDiv(shape: Shape) { @@ -140,6 +180,9 @@ export default class Bubble { //contentHolder.fillColor = new Color("cyan"); contentHolder.strokeWidth = 0; this.innerShape = shape.clone() as Shape; + this.innerShape.remove(); // Removes it from the current (lower) layer. + this.upperLayer.addChild(this.innerShape); + //this.innerShape.bringToFront(); this.innerShape.fillColor = Comical.backColor; const adjustSize = () => { @@ -167,6 +210,9 @@ export default class Bubble { this.shape.position = contentCenter; this.innerShape.position = contentCenter; + // TODO: This still produces some minor imperfections in the border. Think of how to make it look prettier. + this.innerShape.strokeWidth = 0; // Get rid of the outline + // Draw tails, if necessary if (this.spec.tips.length > 0) { this.spec.tips.forEach(tail => { @@ -213,11 +259,13 @@ export default class Bubble { return svg; } - private static loadShape( + public loadShape( 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. project!.importSVG(svg, { onLoad: (item: Item) => { onShapeLoaded(item as Shape); @@ -225,13 +273,24 @@ 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, tip: Point ): Tail { - const tipHandle = Bubble.makeHandle(tip); - const curveHandle = Bubble.makeHandle(mid); + const tipHandle = this.makeHandle(tip); + const curveHandle = this.makeHandle(mid); + this.upperLayer.activate(); let tail = new Tail( start, tipHandle.position!, @@ -291,7 +350,8 @@ export default class Bubble { // TODO: Help? where should I be? I think this comes up with unique names. static handleIndex = 0; - static makeHandle(tip: Point): Path.Circle { + private makeHandle(tip: Point): Path.Circle { + this.handleLayer.activate(); const result = new Path.Circle(tip, 8); result.strokeColor = new Color("aqua"); result.strokeWidth = 2; diff --git a/src/comical.ts b/src/comical.ts index f32de9f..204df50 100644 --- a/src/comical.ts +++ b/src/comical.ts @@ -1,4 +1,4 @@ -import { Color, project, setup } from "paper"; +import { Color, project, setup, Layer } from "paper"; import Bubble from "./bubble"; import { uniqueIds } from "./uniqueId"; @@ -46,7 +46,13 @@ export default class Comical { // call after adding or deleting elements with data-bubble // assumes convertBubbleJsonToCanvas has been called and canvas exists public static update(parent: HTMLElement) { - project!.activeLayer.removeChildren(); + while (project!.layers.length > 1) { + project!.layers.pop(); + } + if (project!.layers.length > 0) { + project!.layers[0].activate(); + } + const elements = parent.ownerDocument!.evaluate( ".//*[@data-bubble]", parent, @@ -54,13 +60,52 @@ export default class Comical { XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); + // Enhance: we want to be able to make all the bubbles and all the tails // 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 = []; for (let i = 0; i < elements.snapshotLength; i++) { const element = elements.snapshotItem(i) as HTMLElement; const bubble = Bubble.getInstance(element); + bubbleList.push(bubble); + + let zLevel = 0; + if (bubble.spec.level) { + zLevel = bubble.spec.level; + } + zLevelList.push(zLevel); + } + + // Ensure that they are in ascending order + zLevelList.sort(); + + // First we need to create all the layers in order. (Because they automatically get added to the end of the project's list of layers + const levelToLayer = {}; + for (let i = 0; i < zLevelList.length; ++i) { + // Check if different than previous. (Ignore duplicate z-indices) + if (i == 0 || zLevelList[i-1] != zLevelList[i]) { + const zLevel = zLevelList[i]; + var lowerLayer = new Layer(); + var upperLayer = new Layer(); + levelToLayer[zLevel] = [lowerLayer, upperLayer]; + } + } + const handleLayer = new Layer(); + + // Now that the layers are created, we can go back and place objects into the correct layers and ask them to draw themselves. + for (let i = 0; i < bubbleList.length; ++i) { + const bubble = bubbleList[i]; + + let zLevel = 0; + if (bubble.spec.level) { + zLevel = bubble.spec.level; + } + + const [lowerLayer, upperLayer] = levelToLayer[zLevel]; + bubble.setLayers(lowerLayer, upperLayer, handleLayer); bubble.makeShapes(); } } diff --git a/stories/index.stories.ts b/stories/index.stories.ts index 3a40add..e4200b2 100644 --- a/stories/index.stories.ts +++ b/stories/index.stories.ts @@ -221,6 +221,67 @@ storiesOf("bubble-edit", module) button.style.position = "absolute"; button.style.top = "600px"; button.style.left = "0"; + return wrapDiv; + }) + .add("overlapping bubbles", () => { + // A generic picture + // Two bubbles that are merged together (at the same layer) + // Overlapping non-merged bubbles + // Multiple tails on a bubble + const wrapDiv = document.createElement("div"); + wrapDiv.style.position = "relative"; + wrapDiv.style.background = "url('The Moon and The Cap_Page 031.jpg') no-repeat 0/600px"; + wrapDiv.style.height = "600px"; + + var div1 = makeTextBlock(wrapDiv, "This should be the highest layer", 130, 80, 100); + var div2 = makeTextBlock(wrapDiv, "This should be the lowest layer", 130, 150, 100); + + var div3 = makeTextBlock(wrapDiv, "This should be the middle layer", 250, 80, 200); + var div4 = makeTextBlock(wrapDiv, "This should be merged with the other middle layer", 250, 130, 100); + // MakeDefaultTip() needs to see the divs laid out in their eventual positions, + // as does convertBubbleJsonToCanvas. + window.setTimeout(() => { + const bubble1 = Bubble.getInstance(div1); + bubble1.spec = { + version: "1.0", + style: "speech", + tips: [Bubble.makeDefaultTip(div1)], + level: 3 + }; + bubble1.setBubbleSpec(bubble1.spec); + + const bubble2 = Bubble.getInstance(div2); + bubble2.spec = { + version: "1.0", + style: "speech", + tips: [Bubble.makeDefaultTip(div2)], + level: 1 + }; + bubble2.setBubbleSpec(bubble2.spec); + + + const bubble3 = Bubble.getInstance(div3); + bubble3.spec = { + version: "1.0", + style: "speech", + tips: [Bubble.makeDefaultTip(div3)], + level: 2 + }; + bubble3.setBubbleSpec(bubble3.spec); + + const bubble4 = Bubble.getInstance(div4); + bubble4.spec = { + version: "1.0", + style: "speech", + tips: [Bubble.makeDefaultTip(div4)], + level: 2 + }; + bubble4.setBubbleSpec(bubble4.spec); + + Comical.convertBubbleJsonToCanvas(wrapDiv); + }, 200); + + return wrapDiv; }); diff --git a/storyStatic/The Moon and The Cap_Page 031.jpg b/storyStatic/The Moon and The Cap_Page 031.jpg new file mode 100644 index 0000000..36cee87 Binary files /dev/null and b/storyStatic/The Moon and The Cap_Page 031.jpg differ