| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Software Design Principles for Novel Writing</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://d3js.org/d3.v7.min.js"></script> |
| <style> |
| .node circle { |
| fill: #fff; |
| stroke: steelblue; |
| stroke-width: 2px; |
| } |
| .node text { |
| font: 12px sans-serif; |
| } |
| .link { |
| fill: none; |
| stroke: #ccc; |
| stroke-width: 1.5px; |
| } |
| .flow-chart rect { |
| fill: #f0f9ff; |
| stroke: #0284c7; |
| stroke-width: 2px; |
| rx: 5; |
| } |
| .flow-chart text { |
| font: 12px sans-serif; |
| fill: #0369a1; |
| } |
| .flow-chart path { |
| fill: none; |
| stroke: #0284c7; |
| stroke-width: 2px; |
| } |
| .principle-card { |
| transition: all 0.3s ease; |
| } |
| .principle-card:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
| } |
| .tooltip { |
| position: absolute; |
| padding: 8px; |
| background: rgba(0, 0, 0, 0.8); |
| color: white; |
| border-radius: 4px; |
| pointer-events: none; |
| font-size: 12px; |
| max-width: 200px; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 font-sans"> |
| <div class="container mx-auto px-4 py-12"> |
| <header class="text-center mb-16"> |
| <h1 class="text-4xl font-bold text-blue-900 mb-4">Software Design Principles for Novel Writing</h1> |
| <p class="text-lg text-gray-600 max-w-3xl mx-auto"> |
| Applying solid software engineering principles to the craft of writing novels can lead to more structured, |
| maintainable, and engaging stories. |
| </p> |
| </header> |
|
|
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-16"> |
| |
| <div class="principle-card bg-white p-6 rounded-lg shadow-md border-l-4 border-blue-500"> |
| <h3 class="text-xl font-semibold text-blue-800 mb-3">Single Responsibility</h3> |
| <p class="text-gray-600 mb-4"> |
| Each character should have one clear purpose or role in the story, just like a class should have one responsibility. |
| </p> |
| <div id="srp-chart" class="h-40"></div> |
| </div> |
|
|
| <div class="principle-card bg-white p-6 rounded-lg shadow-md border-l-4 border-green-500"> |
| <h3 class="text-xl font-semibold text-green-800 mb-3">Open/Closed Principle</h3> |
| <p class="text-gray-600 mb-4"> |
| Your story structure should be open for extension (new plot lines) but closed for modification (core themes remain unchanged). |
| </p> |
| <div id="ocp-chart" class="h-40"></div> |
| </div> |
|
|
| <div class="principle-card bg-white p-6 rounded-lg shadow-md border-l-4 border-purple-500"> |
| <h3 class="text-xl font-semibold text-purple-800 mb-3">Liskov Substitution</h3> |
| <p class="text-gray-600 mb-4"> |
| Character archetypes should be substitutable - a mentor character should fulfill the same narrative role whether young or old. |
| </p> |
| <div id="lsp-chart" class="h-40"></div> |
| </div> |
|
|
| <div class="principle-card bg-white p-6 rounded-lg shadow-md border-l-4 border-yellow-500"> |
| <h3 class="text-xl font-semibold text-yellow-800 mb-3">Interface Segregation</h3> |
| <p class="text-gray-600 mb-4"> |
| Don't force readers to engage with unnecessary subplots. Keep narrative interfaces focused and relevant. |
| </p> |
| <div id="isp-chart" class="h-40"></div> |
| </div> |
|
|
| <div class="principle-card bg-white p-6 rounded-lg shadow-md border-l-4 border-red-500"> |
| <h3 class="text-xl font-semibold text-red-800 mb-3">Dependency Inversion</h3> |
| <p class="text-gray-600 mb-4"> |
| High-level themes should not depend on low-level plot details. Both should depend on abstractions (universal human experiences). |
| </p> |
| <div id="dip-chart" class="h-40"></div> |
| </div> |
|
|
| <div class="principle-card bg-white p-6 rounded-lg shadow-md border-l-4 border-indigo-500"> |
| <h3 class="text-xl font-semibold text-indigo-800 mb-3">DRY Principle</h3> |
| <p class="text-gray-600 mb-4"> |
| Avoid repetitive descriptions and redundant scenes. Each element should have a unique purpose in advancing the story. |
| </p> |
| <div id="dry-chart" class="h-40"></div> |
| </div> |
| </div> |
|
|
| <section class="bg-white rounded-lg shadow-md p-8 mb-16"> |
| <h2 class="text-2xl font-bold text-blue-900 mb-6">Novel Writing Workflow</h2> |
| <div id="workflow-chart" class="h-96"></div> |
| </section> |
|
|
| <section class="bg-white rounded-lg shadow-md p-8 mb-16"> |
| <h2 class="text-2xl font-bold text-blue-900 mb-6">Character Dependency Graph</h2> |
| <p class="text-gray-600 mb-6"> |
| How characters interact in a story mirrors how objects interact in software. Hover over nodes to see details. |
| </p> |
| <div id="character-graph" class="h-96 border rounded-lg bg-gray-50"></div> |
| </section> |
|
|
| <section class="bg-white rounded-lg shadow-md p-8"> |
| <h2 class="text-2xl font-bold text-blue-900 mb-6">Plot Structure Visualization</h2> |
| <p class="text-gray-600 mb-6"> |
| A well-structured plot follows patterns similar to well-designed software architecture. |
| </p> |
| <div id="plot-structure" class="h-96"></div> |
| </section> |
| </div> |
|
|
| <div class="tooltip" style="opacity: 0;"></div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const srpData = [ |
| {principle: "Single Responsibility", character: "Protagonist", responsibility: "Drive main plot"}, |
| {principle: "Single Responsibility", character: "Sidekick", responsibility: "Provide comic relief"}, |
| {principle: "Single Responsibility", character: "Antagonist", responsibility: "Create conflict"}, |
| {principle: "Single Responsibility", character: "Mentor", responsibility: "Provide wisdom"} |
| ]; |
| |
| const srpSvg = d3.select("#srp-chart") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%"); |
| |
| srpSvg.selectAll("circle") |
| .data(srpData) |
| .enter() |
| .append("circle") |
| .attr("cx", (d, i) => 50 + i * 70) |
| .attr("cy", 70) |
| .attr("r", 25) |
| .attr("fill", "#93c5fd") |
| .attr("stroke", "#1e40af"); |
| |
| srpSvg.selectAll("text") |
| .data(srpData) |
| .enter() |
| .append("text") |
| .text(d => d.character.charAt(0)) |
| .attr("x", (d, i) => 50 + i * 70) |
| .attr("y", 75) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#1e40af") |
| .attr("font-weight", "bold"); |
| |
| |
| const ocpSvg = d3.select("#ocp-chart") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%"); |
| |
| ocpSvg.append("rect") |
| .attr("x", 30) |
| .attr("y", 30) |
| .attr("width", 120) |
| .attr("height", 80) |
| .attr("fill", "#a7f3d0") |
| .attr("stroke", "#065f46"); |
| |
| ocpSvg.append("text") |
| .text("Core Theme") |
| .attr("x", 90) |
| .attr("y", 70) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#065f46"); |
| |
| ocpSvg.selectAll("circle") |
| .data([1, 2, 3]) |
| .enter() |
| .append("circle") |
| .attr("cx", d => 30 + d * 30) |
| .attr("cy", 120) |
| .attr("r", 10) |
| .attr("fill", "#6ee7b7") |
| .attr("stroke", "#065f46"); |
| |
| |
| const lspSvg = d3.select("#lsp-chart") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%"); |
| |
| const mentorData = [ |
| {type: "Wise Old Wizard", traits: "Magic, Experience"}, |
| {type: "Retired Warrior", traits: "Combat, Strategy"}, |
| {type: "Scholar", traits: "Knowledge, Research"} |
| ]; |
| |
| lspSvg.selectAll("rect") |
| .data(mentorData) |
| .enter() |
| .append("rect") |
| .attr("x", (d, i) => 20 + i * 80) |
| .attr("y", 40) |
| .attr("width", 70) |
| .attr("height", 50) |
| .attr("rx", 5) |
| .attr("fill", "#d8b4fe") |
| .attr("stroke", "#5b21b6"); |
| |
| lspSvg.selectAll("text") |
| .data(mentorData) |
| .enter() |
| .append("text") |
| .text(d => d.type.split(" ")[0]) |
| .attr("x", (d, i) => 55 + i * 80) |
| .attr("y", 70) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#5b21b6") |
| .attr("font-size", "10px"); |
| |
| |
| const ispSvg = d3.select("#isp-chart") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%"); |
| |
| ispSvg.append("circle") |
| .attr("cx", 90) |
| .attr("cy", 70) |
| .attr("r", 50) |
| .attr("fill", "none") |
| .attr("stroke", "#fcd34d") |
| .attr("stroke-width", 2) |
| .attr("stroke-dasharray", "5,5"); |
| |
| ispSvg.selectAll("path") |
| .data([30, 150, 270]) |
| .enter() |
| .append("path") |
| .attr("d", d => { |
| const x = 90 + 40 * Math.cos(d * Math.PI / 180); |
| const y = 70 + 40 * Math.sin(d * Math.PI / 180); |
| return `M90,70 L${x},${y}`; |
| }) |
| .attr("stroke", "#f59e0b") |
| .attr("stroke-width", 2); |
| |
| ispSvg.selectAll("circle.small") |
| .data([30, 150, 270]) |
| .enter() |
| .append("circle") |
| .attr("cx", d => 90 + 50 * Math.cos(d * Math.PI / 180)) |
| .attr("cy", d => 70 + 50 * Math.sin(d * Math.PI / 180)) |
| .attr("r", 15) |
| .attr("fill", "#fde68a") |
| .attr("stroke", "#b45309"); |
| |
| |
| const dipSvg = d3.select("#dip-chart") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%"); |
| |
| dipSvg.append("rect") |
| .attr("x", 60) |
| .attr("y", 30) |
| .attr("width", 80) |
| .attr("height", 40) |
| .attr("rx", 5) |
| .attr("fill", "#fecaca") |
| .attr("stroke", "#991b1b"); |
| |
| dipSvg.append("text") |
| .text("Themes") |
| .attr("x", 100) |
| .attr("y", 55) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#991b1b"); |
| |
| dipSvg.append("rect") |
| .attr("x", 30) |
| .attr("y", 90) |
| .attr("width", 60) |
| .attr("height", 30) |
| .attr("rx", 5) |
| .attr("fill", "#fca5a5") |
| .attr("stroke", "#991b1b"); |
| |
| dipSvg.append("rect") |
| .attr("x", 110) |
| .attr("y", 90) |
| .attr("width", 60) |
| .attr("height", 30) |
| .attr("rx", 5) |
| .attr("fill", "#fca5a5") |
| .attr("stroke", "#991b1b"); |
| |
| dipSvg.append("text") |
| .text("Plot A") |
| .attr("x", 60) |
| .attr("y", 110) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#991b1b") |
| .attr("font-size", "10px"); |
| |
| dipSvg.append("text") |
| .text("Plot B") |
| .attr("x", 140) |
| .attr("y", 110) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#991b1b") |
| .attr("font-size", "10px"); |
| |
| dipSvg.append("path") |
| .attr("d", "M100,70 L80,90") |
| .attr("stroke", "#991b1b") |
| .attr("stroke-width", 1) |
| .attr("marker-end", "url(#arrow)"); |
| |
| dipSvg.append("path") |
| .attr("d", "M100,70 L120,90") |
| .attr("stroke", "#991b1b") |
| .attr("stroke-width", 1) |
| .attr("marker-end", "url(#arrow)"); |
| |
| |
| const drySvg = d3.select("#dry-chart") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%"); |
| |
| drySvg.selectAll("rect") |
| .data([1, 2, 3]) |
| .enter() |
| .append("rect") |
| .attr("x", d => 30 + d * 40) |
| .attr("y", 40) |
| .attr("width", 30) |
| .attr("height", 60) |
| .attr("rx", 3) |
| .attr("fill", "#bfdbfe") |
| .attr("stroke", "#1e40af"); |
| |
| drySvg.append("rect") |
| .attr("x", 40) |
| .attr("y", 120) |
| .attr("width", 120) |
| .attr("height", 30) |
| .attr("rx", 3) |
| .attr("fill", "#93c5fd") |
| .attr("stroke", "#1e40af"); |
| |
| drySvg.append("text") |
| .text("Shared") |
| .attr("x", 100) |
| .attr("y", 140) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#1e40af"); |
| |
| |
| const workflowSvg = d3.select("#workflow-chart") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%") |
| .classed("flow-chart", true); |
| |
| const workflowData = [ |
| {id: 1, x: 100, y: 50, width: 120, height: 50, text: "Concept Development"}, |
| {id: 2, x: 100, y: 130, width: 120, height: 50, text: "Outline Structure"}, |
| {id: 3, x: 100, y: 210, width: 120, height: 50, text: "Character Design"}, |
| {id: 4, x: 300, y: 50, width: 120, height: 50, text: "First Draft"}, |
| {id: 5, x: 300, y: 130, width: 120, height: 50, text: "Revision"}, |
| {id: 6, x: 300, y: 210, width: 120, height: 50, text: "Editing"}, |
| {id: 7, x: 500, y: 130, width: 120, height: 50, text: "Publishing"} |
| ]; |
| |
| workflowSvg.selectAll("rect") |
| .data(workflowData) |
| .enter() |
| .append("rect") |
| .attr("x", d => d.x) |
| .attr("y", d => d.y) |
| .attr("width", d => d.width) |
| .attr("height", d => d.height) |
| .attr("rx", 5); |
| |
| workflowSvg.selectAll("text") |
| .data(workflowData) |
| .enter() |
| .append("text") |
| .text(d => d.text) |
| .attr("x", d => d.x + d.width/2) |
| .attr("y", d => d.y + d.height/2 + 5) |
| .attr("text-anchor", "middle"); |
| |
| workflowSvg.append("path") |
| .attr("d", "M220,75 L300,75") |
| .attr("marker-end", "url(#arrow)"); |
| |
| workflowSvg.append("path") |
| .attr("d", "M220,155 L300,155") |
| .attr("marker-end", "url(#arrow)"); |
| |
| workflowSvg.append("path") |
| .attr("d", "M420,155 L500,155") |
| .attr("marker-end", "url(#arrow)"); |
| |
| workflowSvg.append("path") |
| .attr("d", "M160,100 L160,130") |
| .attr("marker-end", "url(#arrow)"); |
| |
| workflowSvg.append("path") |
| .attr("d", "M160,180 L160,210") |
| .attr("marker-end", "url(#arrow)"); |
| |
| workflowSvg.append("path") |
| .attr("d", "M360,100 L360,130") |
| .attr("marker-end", "url(#arrow)"); |
| |
| workflowSvg.append("path") |
| .attr("d", "M360,180 L360,210") |
| .attr("marker-end", "url(#arrow)"); |
| |
| |
| const characterData = { |
| nodes: [ |
| {id: 1, name: "Protagonist", role: "Main character", group: 1}, |
| {id: 2, name: "Antagonist", role: "Primary opposition", group: 2}, |
| {id: 3, name: "Mentor", role: "Provides guidance", group: 3}, |
| {id: 4, name: "Love Interest", role: "Romantic subplot", group: 4}, |
| {id: 5, name: "Sidekick", role: "Comic relief/support", group: 5}, |
| {id: 6, name: "Foil", role: "Contrast to protagonist", group: 6} |
| ], |
| links: [ |
| {source: 1, target: 2, value: "Conflict"}, |
| {source: 1, target: 3, value: "Guidance"}, |
| {source: 1, target: 4, value: "Romance"}, |
| {source: 1, target: 5, value: "Support"}, |
| {source: 1, target: 6, value: "Contrast"}, |
| {source: 2, target: 3, value: "Opposes"}, |
| {source: 4, target: 5, value: "Friendship"} |
| ] |
| }; |
| |
| const charSvg = d3.select("#character-graph") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%"); |
| |
| const width = document.getElementById("character-graph").clientWidth; |
| const height = document.getElementById("character-graph").clientHeight; |
| |
| const simulation = d3.forceSimulation(characterData.nodes) |
| .force("link", d3.forceLink(characterData.links).id(d => d.id).distance(100)) |
| .force("charge", d3.forceManyBody().strength(-300)) |
| .force("center", d3.forceCenter(width / 2, height / 2)); |
| |
| const link = charSvg.append("g") |
| .selectAll("line") |
| .data(characterData.links) |
| .enter() |
| .append("line") |
| .attr("stroke-width", 1.5) |
| .attr("stroke", "#9ca3af"); |
| |
| const node = charSvg.append("g") |
| .selectAll("circle") |
| .data(characterData.nodes) |
| .enter() |
| .append("g") |
| .call(d3.drag() |
| .on("start", dragstarted) |
| .on("drag", dragged) |
| .on("end", dragended)); |
| |
| node.append("circle") |
| .attr("r", 20) |
| .attr("fill", d => { |
| const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899"]; |
| return colors[d.group - 1]; |
| }); |
| |
| node.append("text") |
| .text(d => d.name.charAt(0)) |
| .attr("dy", 5) |
| .attr("text-anchor", "middle") |
| .attr("fill", "white"); |
| |
| const tooltip = d3.select(".tooltip"); |
| |
| node.on("mouseover", function(event, d) { |
| tooltip.transition() |
| .duration(200) |
| .style("opacity", .9); |
| tooltip.html(`<strong>${d.name}</strong><br/>${d.role}`) |
| .style("left", (event.pageX + 10) + "px") |
| .style("top", (event.pageY - 28) + "px"); |
| }) |
| .on("mouseout", function() { |
| tooltip.transition() |
| .duration(500) |
| .style("opacity", 0); |
| }); |
| |
| simulation.on("tick", () => { |
| link |
| .attr("x1", d => d.source.x) |
| .attr("y1", d => d.source.y) |
| .attr("x2", d => d.target.x) |
| .attr("y2", d => d.target.y); |
| |
| node |
| .attr("transform", d => `translate(${d.x},${d.y})`); |
| }); |
| |
| function dragstarted(event, d) { |
| if (!event.active) simulation.alphaTarget(0.3).restart(); |
| d.fx = d.x; |
| d.fy = d.y; |
| } |
| |
| function dragged(event, d) { |
| d.fx = event.x; |
| d.fy = event.y; |
| } |
| |
| function dragended(event, d) { |
| if (!event.active) simulation.alphaTarget(0); |
| d.fx = null; |
| d.fy = null; |
| } |
| |
| |
| const plotData = [ |
| {x: 0, y: 100, label: "Exposition"}, |
| {x: 150, y: 50, label: "Inciting Incident"}, |
| {x: 300, y: 200, label: "Rising Action"}, |
| {x: 450, y: 50, label: "Climax"}, |
| {x: 600, y: 150, label: "Falling Action"}, |
| {x: 750, y: 100, label: "Resolution"} |
| ]; |
| |
| const plotSvg = d3.select("#plot-structure") |
| .append("svg") |
| .attr("width", "100%") |
| .attr("height", "100%"); |
| |
| const plotWidth = document.getElementById("plot-structure").clientWidth; |
| const plotHeight = document.getElementById("plot-structure").clientHeight; |
| |
| const xScale = d3.scaleLinear() |
| .domain([0, 750]) |
| .range([50, plotWidth - 50]); |
| |
| const yScale = d3.scaleLinear() |
| .domain([0, 200]) |
| .range([plotHeight - 100, 50]); |
| |
| const line = d3.line() |
| .x(d => xScale(d.x)) |
| .y(d => yScale(d.y)) |
| .curve(d3.curveBasis); |
| |
| plotSvg.append("path") |
| .datum(plotData) |
| .attr("d", line) |
| .attr("fill", "none") |
| .attr("stroke", "#3b82f6") |
| .attr("stroke-width", 3); |
| |
| plotSvg.selectAll("circle") |
| .data(plotData) |
| .enter() |
| .append("circle") |
| .attr("cx", d => xScale(d.x)) |
| .attr("cy", d => yScale(d.y)) |
| .attr("r", 6) |
| .attr("fill", "#1d4ed8"); |
| |
| plotSvg.selectAll("text") |
| .data(plotData) |
| .enter() |
| .append("text") |
| .text(d => d.label) |
| .attr("x", d => xScale(d.x)) |
| .attr("y", d => yScale(d.y) - 15) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#1e40af") |
| .attr("font-size", "12px"); |
| |
| |
| plotSvg.append("defs").append("marker") |
| .attr("id", "arrow") |
| .attr("viewBox", "0 -5 10 10") |
| .attr("refX", 10) |
| .attr("refY", 0) |
| .attr("markerWidth", 6) |
| .attr("markerHeight", 6) |
| .attr("orient", "auto") |
| .append("path") |
| .attr("d", "M0,-5L10,0L0,5") |
| .attr("fill", "#999"); |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=arirajuns/writing1" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |