Skip to content
Draft
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
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ tasks.register<Sync>("site") {
into("basic") {
from(project(":examples:basic").tasks.named("wasmJsBrowserDistribution"))
}
into("diagram") {
from(project(":examples:diagram").tasks.named("wasmJsBrowserDistribution"))
}
into("interactive") {
from(project(":examples:interactive").tasks.named("wasmJsBrowserDistribution"))
}
Expand Down
46 changes: 46 additions & 0 deletions examples/diagram/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl

plugins {
kotlin("multiplatform")
kotlin("plugin.compose")
id("org.jetbrains.compose")
}

group = "dev.bnorm.storyboard.example"
version = "0.1-SNAPSHOT"

kotlin {
jvm()

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
binaries.executable()
browser()
}

sourceSets {
commonMain {
dependencies {
implementation(project(":storyboard"))
implementation(project(":storyboard-easel"))
implementation(project(":storyboard-layout"))
implementation(project(":storyboard-text"))

implementation(project(":examples:shared"))

implementation(compose.material)
implementation(compose.components.resources)
}
}
jvmMain {
dependencies {
implementation(compose.desktop.currentOs)
}
}
}
}

compose {
resources.publicResClass = true
desktop.application.mainClass = "Main_desktopKt"
}
193 changes: 193 additions & 0 deletions examples/diagram/src/commonMain/kotlin/ConnectedTree.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import dev.bnorm.storyboard.StoryboardBuilder
import dev.bnorm.storyboard.easel.rememberSharedContentState
import dev.bnorm.storyboard.easel.sharedElement
import dev.bnorm.storyboard.example.shared.Bullet
import dev.bnorm.storyboard.layout.decorated.CubicLine
import dev.bnorm.storyboard.layout.decorated.Decoration
import dev.bnorm.storyboard.layout.decorated.DecorationLayout
import dev.bnorm.storyboard.layout.tree.HorizontalTree
import dev.bnorm.storyboard.toState

fun StoryboardBuilder.ConnectedTree() {
val root = Node(
index = 0,
Node(
index = 1,
Node(2), Node(3)
),
Node(
index = 4,
Node(5), Node(6)
)
)

class State(val root: Node?)

val indexes = buildList { root.collect { add(it.index) } }.sorted()
val states = buildList {
add(State(null))
for (index in indexes) {
add(State(root.filter { it <= index }))
}
}

scene(states) {
val state = transition.createChildTransition { it.toState() }

Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
Text("Connected Tree", style = MaterialTheme.typography.h3)
Box(
Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
.height(2.dp)
.background(Color.Black)
)
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.weight(1f)
) {
ProvideTextStyle(MaterialTheme.typography.h5) {
Bullet("`HorizontalTree` can be used to layout tree structures along the horizontal axis.")
Bullet("`DecorationLayout` can be used to \"decorate\" `Composables`:")
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(start = 32.dp)
) {
ProvideTextStyle(MaterialTheme.typography.h6) {
Bullet("Can connect `Composables` with lines, curves, or arrows.")
Bullet("Can surround `Composables` with generic shapes.")
Bullet("Can outline `Composables` with specialized borders.")
Bullet("A decoration is simply a `Canvas` which knows the bounding box of keyed `Composable`s.")
}
}
}
}
Surface(
shape = RoundedCornerShape(16.dp),
border = BorderStroke(2.dp, Color.Black),
color = Color.LightGray,
elevation = 8.dp,
modifier = Modifier.weight(1f)
) {
SharedTransitionLayout {
state.AnimatedContent(
transitionSpec = {
fadeIn(tween(300, easing = EaseIn))
.togetherWith(fadeOut(tween(300, easing = EaseOut)))
},
) {
when (val root = it.root) {
null -> Box(modifier = Modifier.fillMaxSize())
else -> NodeTree(root, modifier = Modifier.fillMaxSize())
}
}
}
}
}
}
}
}

private class Node(val index: Int, val children: List<Node> = emptyList()) {
constructor(index: Int, vararg children: Node) : this(index, children.toList())
}

private fun Node.collect(block: (Node) -> Unit) {
block(this)
children.forEach { it.collect(block) }
}

private fun Node.filter(predicate: (Int) -> Boolean): Node? {
if (!predicate(index)) return null

val filtered = children.mapNotNull { child -> child.filter(predicate) }
return Node(index, filtered)
}

@Composable
context(_: AnimatedVisibilityScope, _: SharedTransitionScope)
private fun NodeTree(
root: Node,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current

@Composable
fun buildDecorations(node: Node): List<Decoration> = buildList {
for (child in node.children) {
add(
CubicLine(
startKey = node.index,
startAlignment = Alignment.CenterEnd,
endKey = child.index,
endAlignment = Alignment.CenterStart,
color = Color.Black,
stroke = Stroke(with(density) { 2.dp.toPx() }),
modifier = Modifier.sharedElement(
rememberSharedContentState("${node.index}-${child.index}"),
boundsTransform = BoundsTransform { _, _ -> tween(300, easing = EaseInOut) },
)
)
)
addAll(buildDecorations(child))
}
}

DecorationLayout(
decorations = buildDecorations(root),
modifier = modifier
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
HorizontalTree(
root = root,
getChildren = { it.children },
horizontalArrangement = Arrangement.spacedBy(32.dp),
) {
key(it.index) {
Surface(
border = BorderStroke(2.dp, Color.Black),
shape = RoundedCornerShape(8.dp),
elevation = 8.dp,
modifier = Modifier
.sharedElement(
rememberSharedContentState(it.index),
boundsTransform = BoundsTransform { _, _ -> tween(300, easing = EaseInOut) },
)
.decorate(it.index)
) {
Text(
text = "Node : ${it.index}",
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
)
}
}
}
}
}
}
27 changes: 27 additions & 0 deletions examples/diagram/src/commonMain/kotlin/Storyboard.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Typography
import androidx.compose.material.lightColors
import dev.bnorm.storyboard.SceneDecorator
import dev.bnorm.storyboard.SceneFormat
import dev.bnorm.storyboard.Storyboard

fun createStoryboard(): Storyboard {
return Storyboard.build(
title = "Diagram Storyboard",
format = SceneFormat.Default,
decorator = theme,
) {
ConnectedTree()
}
}

private val theme = SceneDecorator { content ->
val colors = lightColors()
val typography = Typography()
MaterialTheme(colors, typography) {
Surface {
content()
}
}
}
13 changes: 13 additions & 0 deletions examples/diagram/src/jvmMain/kotlin/main.desktop.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.ui.window.application
import dev.bnorm.storyboard.easel.DesktopStoryEasel

fun main() {
val storyboard = createStoryboard()
application {
MaterialTheme(colors = darkColors()) {
DesktopStoryEasel(storyboard)
}
}
}
19 changes: 19 additions & 0 deletions examples/diagram/src/wasmJsMain/kotlin/main.web.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
import dev.bnorm.storyboard.easel.WebStoryEasel
import kotlinx.browser.document
import org.w3c.dom.HTMLCanvasElement

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val storyboard = createStoryboard()
val element = document.getElementById("ComposeTarget") as HTMLCanvasElement
element.focus() // Focus is required for keyboard navigation.
CanvasBasedWindow(canvasElementId = element.id, title = storyboard.title) {
MaterialTheme(colors = darkColors()) {
WebStoryEasel(storyboard)
}
}
}
12 changes: 12 additions & 0 deletions examples/diagram/src/wasmJsMain/resources/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<title>Basic Storyboard</title>
<script src="diagram.js"></script>
</head>
<body>
<canvas id="ComposeTarget"></canvas>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.bnorm.storyboard.example.shared

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp

@Composable
fun Bullet(text: String) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("•")
Text(text)
}
}
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ rootProject.name = "storyboard-root"

include(":storyboard")
include(":storyboard-easel")
include(":storyboard-layout")
include(":storyboard-text")

include(":examples:basic")
include(":examples:diagram")
include(":examples:interactive")
include(":examples:shared")
27 changes: 27 additions & 0 deletions storyboard-layout/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl

plugins {
kotlin("multiplatform")
kotlin("plugin.compose")
id("org.jetbrains.compose")
id("com.vanniktech.maven.publish")
}

group = "dev.bnorm.storyboard"

kotlin {
jvm()

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}

sourceSets {
commonMain {
dependencies {
implementation(compose.material)
}
}
}
}
Loading
Loading